blob: c15444e24fcca29671fd03a42d02d9e462e475b0 [file] [log] [blame]
* Copyright 2023 Google LLC
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
#include "src/core/SkFilterColorProgram.h"
#include "include/core/SkColorFilter.h"
#include "include/effects/SkRuntimeEffect.h"
#include "src/core/SkVM.h"
#include "src/sksl/SkSLAnalysis.h"
#include "src/sksl/analysis/SkSLProgramUsage.h"
#include "src/sksl/codegen/SkSLVMCodeGenerator.h"
#include "src/sksl/ir/SkSLProgram.h"
using namespace skia_private;
#if defined(SK_ENABLE_SKSL) && defined(SK_ENABLE_SKVM)
std::unique_ptr<SkFilterColorProgram> SkFilterColorProgram::Make(const SkRuntimeEffect* effect) {
// Our per-effect program technique is only possible (and necessary) for color filters
if (!effect->allowColorFilter()) {
return nullptr;
// TODO(skia:10479): Can we support this? When the color filter is invoked like this, there
// may not be a real working space? If there is, we'd need to add it as a parameter to eval,
// and then coordinate where the relevant uniforms go. For now, just fall back to the slow
// path if we see these intrinsics being called.
if (effect->usesColorTransform()) {
return nullptr;
// We require that any children are 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(),
[](const SkRuntimeEffect::Child& c) {
return c.type == SkRuntimeEffect::ChildType::kColorFilter;
})) {
return nullptr;
skvm::Builder p;
// For SkSL uniforms, we reserve space and allocate skvm Uniform ids for each one. When we run
// the program, these ids will be loads from the *first* arg ptr, the uniform data of the
// specific color filter instance.
skvm::Uniforms skslUniforms{p.uniform(), 0};
const size_t uniformCount = effect->uniformSize() / 4;
std::vector<skvm::Val> uniform;
for (size_t i = 0; i < uniformCount; i++) {
uniform.push_back(p.uniform32(skslUniforms.push(/*placeholder*/ 0)).id);
// We reserve a uniform color for each child invocation. While processing the SkSL, we record
// the index of the child, and the color being filtered (in a SampleCall struct).
// When we run this program later, we use the SampleCall to evaluate the correct child, and
// populate these uniform values. These Uniform ids are loads from the *second* arg ptr.
// If the color being passed is too complex for us to describe and re-create using SampleCall,
// we are unable to use this per-effect program, and callers will need to fall back to another
// (slower) implementation.
skvm::Uniforms childColorUniforms{p.uniform(), 0};
skvm::Color inputColor = p.uniformColor(/*placeholder*/ SkColors::kWhite, &childColorUniforms);
std::vector<SkFilterColorProgram::SampleCall> sampleCalls;
class Callbacks : public SkSL::SkVMCallbacks {
Callbacks(skvm::Builder* builder,
const skvm::Uniforms* skslUniforms,
skvm::Uniforms* childColorUniforms,
skvm::Color inputColor,
std::vector<SkFilterColorProgram::SampleCall>* sampleCalls)
: fBuilder(builder)
, fSkslUniforms(skslUniforms)
, fChildColorUniforms(childColorUniforms)
, fInputColor(inputColor)
, fSampleCalls(sampleCalls) {}
bool isSimpleUniform(skvm::Color c, int* baseOffset) {
skvm::Uniform ur, ug, ub, ua;
if (!fBuilder->allUniform(, &ur,, &ug,, &ub,, &ua)) {
return false;
skvm::Ptr uniPtr = fSkslUniforms->base;
if (ur.ptr != uniPtr || ug.ptr != uniPtr || ub.ptr != uniPtr || ua.ptr != uniPtr) {
return false;
*baseOffset = ur.offset;
return ug.offset == ur.offset + 4 &&
ub.offset == ur.offset + 8 &&
ua.offset == ur.offset + 12;
static bool IDsEqual(skvm::Color x, skvm::Color y) {
return == && == && == && ==;
skvm::Color sampleColorFilter(int ix, skvm::Color c) override {
skvm::Color result =
fBuilder->uniformColor(/*placeholder*/ SkColors::kWhite, fChildColorUniforms);
SkFilterColorProgram::SampleCall call;
call.fChild = ix;
if (IDsEqual(c, fInputColor)) {
call.fKind = SkFilterColorProgram::SampleCall::Kind::kInputColor;
} else if (fBuilder->allImm(, &call.fImm.fR,, &call.fImm.fG,, &call.fImm.fB,, &call.fImm.fA)) {
call.fKind = SkFilterColorProgram::SampleCall::Kind::kImmediate;
} else if (auto it = std::find_if(fChildColors.begin(),
[&](skvm::Color x) { return IDsEqual(x, c); });
it != fChildColors.end()) {
call.fKind = SkFilterColorProgram::SampleCall::Kind::kPrevious;
call.fPrevious = SkTo<int>(it - fChildColors.begin());
} else if (isSimpleUniform(c, &call.fOffset)) {
call.fKind = SkFilterColorProgram::SampleCall::Kind::kUniform;
} else {
fAllSampleCallsSupported = false;
return result;
// We did an early return from this function if we saw any child that wasn't a shader, so
// it should be impossible for either of these callbacks to occur:
skvm::Color sampleShader(int, skvm::Coord) override {
SkDEBUGFAIL("Unexpected child type");
return {};
skvm::Color sampleBlender(int, skvm::Color, skvm::Color) override {
SkDEBUGFAIL("Unexpected child type");
return {};
// We did an early return from this function if we saw any call to these intrinsics, so it
// should be impossible for either of these callbacks to occur:
skvm::Color toLinearSrgb(skvm::Color color) override {
SkDEBUGFAIL("Unexpected color transform intrinsic");
return {};
skvm::Color fromLinearSrgb(skvm::Color color) override {
SkDEBUGFAIL("Unexpected color transform intrinsic");
return {};
skvm::Builder* fBuilder;
const skvm::Uniforms* fSkslUniforms;
skvm::Uniforms* fChildColorUniforms;
skvm::Color fInputColor;
std::vector<SkFilterColorProgram::SampleCall>* fSampleCalls;
std::vector<skvm::Color> fChildColors;
bool fAllSampleCallsSupported = true;
Callbacks callbacks(&p, &skslUniforms, &childColorUniforms, inputColor, &sampleCalls);
// Emit the skvm instructions for the SkSL
skvm::Coord zeroCoord = {p.splat(0.0f), p.splat(0.0f)};
skvm::Color result = SkSL::ProgramToSkVM(*effect->fBaseProgram,
// Then store the result to the *third* arg ptr{skvm::PixelFormat::FLOAT, 32, 32, 32, 32, 0, 32, 64, 96},
p.varying<skvm::F32>(), result);
if (!callbacks.fAllSampleCallsSupported) {
return nullptr;
// We'll use this program to filter one color at a time, don't bother with jit
return std::unique_ptr<SkFilterColorProgram>(
new SkFilterColorProgram(p.done(/*debug_name=*/nullptr, /*allow_jit=*/false),
SkFilterColorProgram::SkFilterColorProgram(skvm::Program program,
std::vector<SampleCall> sampleCalls)
: fProgram(std::move(program))
, fSampleCalls(std::move(sampleCalls)) {}
SkPMColor4f SkFilterColorProgram::eval(
const SkPMColor4f& inColor,
const void* uniformData,
std::function<SkPMColor4f(int, SkPMColor4f)> evalChild) const {
// Our program defines sampling any child as returning a uniform color. Assemble a buffer
// containing those colors. The first entry is always the input color. Subsequent entries
// are for each sample call, based on the information in fSampleCalls. For any null children,
// the sample result is just the passed-in color.
STArray<4, SkPMColor4f, true> childColors;
for (const auto& s : fSampleCalls) {
SkPMColor4f passedColor = inColor;
switch (s.fKind) {
case SampleCall::Kind::kInputColor: break;
case SampleCall::Kind::kImmediate: passedColor = s.fImm; break;
case SampleCall::Kind::kPrevious: passedColor = childColors[s.fPrevious + 1]; break;
case SampleCall::Kind::kUniform:
passedColor = *SkTAddOffset<const SkPMColor4f>(uniformData, s.fOffset);
childColors.push_back(evalChild(s.fChild, passedColor));
SkPMColor4f result;
fProgram.eval(1, uniformData, childColors.begin(), result.vec());
return result;
#endif // defined(SK_ENABLE_SKSL) && defined(SK_ENABLE_SKVM)