Runtime effects: Detect passthrough sample calls automatically
As explained, this is *very* conservative. It only works when the child
is sampled from within main, and using a direct reference to the coords
parameter. If that parameter is ever modified (even after being used),
the optimization doesn't happen. For most cases, this is fine.
Bug: skia:11869
Change-Id: Ia06181730a6d07e2a4fe2de4cc8e8c3402f0dc52
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/397320
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: John Stiles <johnstiles@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/gm/runtimeeffectimage.cpp b/gm/runtimeeffectimage.cpp
index c9f8940..b9cb610 100644
--- a/gm/runtimeeffectimage.cpp
+++ b/gm/runtimeeffectimage.cpp
@@ -35,7 +35,7 @@
half b = fract((p.x + 5)/10);
half a = min(distance(p, vec2(50, 50))/50 + 0.3, 1);
half4 result = half4(r, g, b, a);
- result *= sample(child);
+ result *= sample(child, p);
if (gAlphaType == 0) {
result.rgb *= a;
}
diff --git a/gm/runtimeshader.cpp b/gm/runtimeshader.cpp
index 5ec6cd7..578f28a 100644
--- a/gm/runtimeshader.cpp
+++ b/gm/runtimeshader.cpp
@@ -134,10 +134,10 @@
}
half4 main(float2 xy) {
- half4 before = sample(before_map);
- half4 after = sample(after_map);
+ half4 before = sample(before_map, xy);
+ half4 after = sample(after_map, xy);
- float m = smooth_cutoff(sample(threshold_map).a);
+ float m = smooth_cutoff(sample(threshold_map, xy).a);
return mix(before, after, m);
}
)", kAnimate_RTFlag | kBench_RTFlag) {}
@@ -228,7 +228,7 @@
uniform float inv_size;
half4 main(float2 xy) {
- float4 c = unpremul(sample(input));
+ float4 c = unpremul(sample(input, xy));
// Map to cube coords:
float3 cubeCoords = float3(c.rg * rg_scale + rg_bias, c.b * b_scale);
diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp
index 755b3d9..fb8a614 100644
--- a/src/core/SkRuntimeEffect.cpp
+++ b/src/core/SkRuntimeEffect.cpp
@@ -178,7 +178,19 @@
settings.fForceNoInline = options.forceNoInline;
settings.fAllowNarrowingConversions = true;
- const SkSL::FunctionDefinition* main = nullptr;
+ // Find 'main', then locate the sample coords parameter. (It might not be present.)
+ const SkSL::FunctionDefinition* main = SkSL::Program_GetFunction(*program, "main");
+ if (!main) {
+ RETURN_FAILURE("missing 'main' function");
+ }
+ const auto& mainParams = main->declaration().parameters();
+ auto iter = std::find_if(mainParams.begin(), mainParams.end(), [](const SkSL::Variable* p) {
+ return p->modifiers().fLayout.fBuiltin == SK_MAIN_COORDS_BUILTIN;
+ });
+ const SkSL::ProgramUsage::VariableCounts sampleCoordsUsage =
+ iter != mainParams.end() ? program->usage()->get(**iter)
+ : SkSL::ProgramUsage::VariableCounts{};
+
uint32_t flags = 0;
switch (kind) {
case SkSL::ProgramKind::kRuntimeColorFilter: flags |= kAllowColorFilter_Flag; break;
@@ -187,7 +199,9 @@
kAllowShader_Flag); break;
default: SkUNREACHABLE;
}
- if (SkSL::Analysis::ReferencesSampleCoords(*program)) {
+
+
+ if (sampleCoordsUsage.fRead || sampleCoordsUsage.fWrite) {
flags |= kUsesSampleCoords_Flag;
}
@@ -201,7 +215,7 @@
// this can be simpler. There is no way for color filters to refer to sk_FragCoord or sample
// coords in that mode.
if ((flags & kAllowColorFilter_Flag) &&
- (SkSL::Analysis::ReferencesFragCoords(*program) || (flags & kUsesSampleCoords_Flag))) {
+ ((flags & kUsesSampleCoords_Flag) || SkSL::Analysis::ReferencesFragCoords(*program))) {
flags &= ~kAllowColorFilter_Flag;
}
@@ -233,7 +247,8 @@
// Child effects that can be sampled ('shader' or 'colorFilter')
else if (varType.isEffectChild()) {
children.push_back(var.name());
- sampleUsages.push_back(SkSL::Analysis::GetSampleUsage(*program, var));
+ sampleUsages.push_back(SkSL::Analysis::GetSampleUsage(
+ *program, var, sampleCoordsUsage.fWrite != 0));
}
// 'uniform' variables
else if (var.modifiers().fFlags & SkSL::Modifiers::kUniform_Flag) {
@@ -274,18 +289,6 @@
uniforms.push_back(uni);
}
}
- // Functions
- else if (elem->is<SkSL::FunctionDefinition>()) {
- const auto& func = elem->as<SkSL::FunctionDefinition>();
- const SkSL::FunctionDeclaration& decl = func.declaration();
- if (decl.isMain()) {
- main = &func;
- }
- }
- }
-
- if (!main) {
- RETURN_FAILURE("missing 'main' function");
}
#undef RETURN_FAILURE
diff --git a/src/gpu/effects/GrSkSLFP.cpp b/src/gpu/effects/GrSkSLFP.cpp
index aba6a96..50eabbc 100644
--- a/src/gpu/effects/GrSkSLFP.cpp
+++ b/src/gpu/effects/GrSkSLFP.cpp
@@ -40,8 +40,13 @@
FPCallbacks(GrGLSLSkSLFP* self,
EmitArgs& args,
const char* inputColor,
+ const std::vector<SkSL::SampleUsage>& sampleUsages,
const SkSL::Context& context)
- : fSelf(self), fArgs(args), fInputColor(inputColor), fContext(context) {}
+ : fSelf(self)
+ , fArgs(args)
+ , fInputColor(inputColor)
+ , fSampleUsages(sampleUsages)
+ , fContext(context) {}
using String = SkSL::String;
@@ -96,6 +101,21 @@
}
String sampleChild(int index, String coords) override {
+ // If the child was sampled using the coords passed to main (and they are never
+ // modified), then we will have marked the child as PassThrough. The code generator
+ // doesn't know that, and still supplies coords. Inside invokeChild, we assert that
+ // any coords passed for a PassThrough child match args.fSampleCoords exactly.
+ //
+ // Normally, this is valid. Here, we *copied* the sample coords to a local variable
+ // (so that they're mutable in the runtime effect SkSL). Thus, the coords string we
+ // get here is the name of the local copy, and fSampleCoords still points to the
+ // unmodified original (which might be a varying, for example).
+ // To prevent the assert, we pass the empty string in this case. Note that for
+ // children sampled like this, invokeChild doesn't even use the coords parameter,
+ // except for that assert.
+ if (fSampleUsages[index].fPassThrough) {
+ coords.clear();
+ }
return String(fSelf->invokeChild(index, fInputColor, fArgs, coords).c_str());
}
@@ -112,10 +132,11 @@
.c_str());
}
- GrGLSLSkSLFP* fSelf;
- EmitArgs& fArgs;
- const char* fInputColor;
- const SkSL::Context& fContext;
+ GrGLSLSkSLFP* fSelf;
+ EmitArgs& fArgs;
+ const char* fInputColor;
+ const std::vector<SkSL::SampleUsage>& fSampleUsages;
+ const SkSL::Context& fContext;
};
// Snap off a global copy of the input color at the start of main. We need this when
@@ -135,7 +156,8 @@
args.fFragBuilder->codeAppendf("float2 %s = %s;\n", coords, args.fSampleCoord);
}
- FPCallbacks callbacks(this, args, inputColorCopy.c_str(), *program.fContext);
+ FPCallbacks callbacks(
+ this, args, inputColorCopy.c_str(), fp.fEffect->fSampleUsages, *program.fContext);
SkSL::PipelineStage::ConvertProgram(program, coords, args.fInputColor, &callbacks);
}
diff --git a/src/sksl/SkSLAnalysis.cpp b/src/sksl/SkSLAnalysis.cpp
index 75987a6..52301d2 100644
--- a/src/sksl/SkSLAnalysis.cpp
+++ b/src/sksl/SkSLAnalysis.cpp
@@ -74,8 +74,8 @@
// Visitor that determines the merged SampleUsage for a given child 'fp' in the program.
class MergeSampleUsageVisitor : public ProgramVisitor {
public:
- MergeSampleUsageVisitor(const Context& context, const Variable& fp)
- : fContext(context), fFP(fp) {}
+ MergeSampleUsageVisitor(const Context& context, const Variable& fp, bool writesToSampleCoords)
+ : fContext(context), fFP(fp), fWritesToSampleCoords(writesToSampleCoords) {}
SampleUsage visit(const Program& program) {
fUsage = SampleUsage(); // reset to none
@@ -86,6 +86,7 @@
protected:
const Context& fContext;
const Variable& fFP;
+ const bool fWritesToSampleCoords;
SampleUsage fUsage;
bool visitExpression(const Expression& e) override {
@@ -97,7 +98,18 @@
if (fc.arguments().size() >= 2) {
const Expression* coords = fc.arguments()[1].get();
if (coords->type() == *fContext.fTypes.fFloat2) {
- fUsage.merge(SampleUsage::Explicit());
+ // If the coords are a direct reference to the program's sample-coords,
+ // and those coords are never modified, we can conservatively turn this
+ // into PassThrough sampling. In all other cases, we consider it Explicit.
+ if (!fWritesToSampleCoords && coords->is<VariableReference>() &&
+ coords->as<VariableReference>()
+ .variable()
+ ->modifiers()
+ .fLayout.fBuiltin == SK_MAIN_COORDS_BUILTIN) {
+ fUsage.merge(SampleUsage::PassThrough());
+ } else {
+ fUsage.merge(SampleUsage::Explicit());
+ }
} else if (coords->type() == *fContext.fTypes.fFloat3x3) {
// Determine the type of matrix for this call site
if (coords->isConstantOrUniform()) {
@@ -568,8 +580,10 @@
////////////////////////////////////////////////////////////////////////////////
// Analysis
-SampleUsage Analysis::GetSampleUsage(const Program& program, const Variable& fp) {
- MergeSampleUsageVisitor visitor(*program.fContext, fp);
+SampleUsage Analysis::GetSampleUsage(const Program& program,
+ const Variable& fp,
+ bool writesToSampleCoords) {
+ MergeSampleUsageVisitor visitor(*program.fContext, fp, writesToSampleCoords);
return visitor.visit(program);
}
diff --git a/src/sksl/SkSLAnalysis.h b/src/sksl/SkSLAnalysis.h
index 945c287..ee61671 100644
--- a/src/sksl/SkSLAnalysis.h
+++ b/src/sksl/SkSLAnalysis.h
@@ -33,7 +33,14 @@
* Provides utilities for analyzing SkSL statically before it's composed into a full program.
*/
struct Analysis {
- static SampleUsage GetSampleUsage(const Program& program, const Variable& fp);
+ /**
+ * Determines how `program` samples `fp`. By default, assumes that the sample coords
+ * (SK_MAIN_COORDS_BUILTIN) might be modified, so `sample(fp, sampleCoords)` is treated as
+ * Explicit. If writesToSampleCoords is false, treats that as PassThrough, instead.
+ */
+ static SampleUsage GetSampleUsage(const Program& program,
+ const Variable& fp,
+ bool writesToSampleCoords = true);
static bool ReferencesBuiltin(const Program& program, int builtin);
diff --git a/src/sksl/ir/SkSLProgram.h b/src/sksl/ir/SkSLProgram.h
index e44497e..152c6a6 100644
--- a/src/sksl/ir/SkSLProgram.h
+++ b/src/sksl/ir/SkSLProgram.h
@@ -199,6 +199,8 @@
return result;
}
+ const ProgramUsage* usage() const { return fUsage.get(); }
+
std::unique_ptr<String> fSource;
std::unique_ptr<ProgramConfig> fConfig;
std::shared_ptr<Context> fContext;
diff --git a/tests/SkRuntimeEffectTest.cpp b/tests/SkRuntimeEffectTest.cpp
index 23f312d..c32ec62 100644
--- a/tests/SkRuntimeEffectTest.cpp
+++ b/tests/SkRuntimeEffectTest.cpp
@@ -16,6 +16,7 @@
#include "src/core/SkColorSpacePriv.h"
#include "src/core/SkTLazy.h"
#include "src/gpu/GrColor.h"
+#include "src/gpu/GrFragmentProcessor.h"
#include "tests/Test.h"
#include <algorithm>
@@ -432,3 +433,51 @@
REPORTER_ASSERT(r, filter && !filter->isAlphaUnchanged());
}
}
+
+DEF_TEST(SkRuntimeShaderSampleUsage, r) {
+ auto test = [&](const char* src, bool expectExplicit) {
+ auto [effect, err] =
+ SkRuntimeEffect::MakeForShader(SkStringPrintf("uniform shader child; %s", src));
+ REPORTER_ASSERT(r, effect);
+
+ auto child = GrFragmentProcessor::MakeColor({ 1, 1, 1, 1 });
+ auto fp = effect->makeFP(nullptr, &child, 1);
+ REPORTER_ASSERT(r, fp);
+
+ REPORTER_ASSERT(r, fp->childProcessor(0)->isSampledWithExplicitCoords() == expectExplicit);
+ };
+
+ // This test verifies that we detect calls to sample where the coords are the same as those
+ // passed to main. In those cases, it's safe to turn the "explicit" sampling into "passthrough"
+ // sampling. This optimization is implemented very conservatively.
+
+ // Cases where our optimization is valid, and works:
+
+ // Direct use of passed-in coords
+ test("half4 main(float2 xy) { return sample(child, xy); }", false);
+ // Sample with passed-in coords, read (but don't write) sample coords elsewhere
+ test("half4 main(float2 xy) { return sample(child, xy) + sin(xy.x); }", false);
+
+ // Cases where our optimization is not valid, and does not happen:
+
+ // Sampling with values completely unrelated to passed-in coords
+ test("half4 main(float2 xy) { return sample(child, float2(0, 0)); }", true);
+ // Use of expression involving passed in coords
+ test("half4 main(float2 xy) { return sample(child, xy * 0.5); }", true);
+ // Use of coords after modification
+ test("half4 main(float2 xy) { xy *= 2; return sample(child, xy); }", true);
+ // Use of coords after modification via out-param call
+ test("void adjust(inout float2 xy) { xy *= 2; }"
+ "half4 main(float2 xy) { adjust(xy); return sample(child, xy); }", true);
+
+ // There should (must) not be any false-positive cases. There are false-negatives.
+ // In all of these cases, our optimization would be valid, but does not happen:
+
+ // Direct use of passed-in coords, modified after use
+ test("half4 main(float2 xy) { half4 c = sample(child, xy); xy *= 2; return c; }", true);
+ // Passed-in coords copied to a temp variable
+ test("half4 main(float2 xy) { float2 p = xy; return sample(child, p); }", true);
+ // Use of coords passed to helper function
+ test("half4 helper(float2 xy) { return sample(child, xy); }"
+ "half4 main(float2 xy) { return helper(xy); }", true);
+}