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>
This commit is contained in:
parent
d0f35c241c
commit
cc3d2d25c5
@ -35,7 +35,7 @@ protected:
|
||||
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;
|
||||
}
|
||||
|
@ -134,10 +134,10 @@ public:
|
||||
}
|
||||
|
||||
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 @@ public:
|
||||
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);
|
||||
|
@ -178,7 +178,19 @@ SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl,
|
||||
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 @@ SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl,
|
||||
kAllowShader_Flag); break;
|
||||
default: SkUNREACHABLE;
|
||||
}
|
||||
if (SkSL::Analysis::ReferencesSampleCoords(*program)) {
|
||||
|
||||
|
||||
if (sampleCoordsUsage.fRead || sampleCoordsUsage.fWrite) {
|
||||
flags |= kUsesSampleCoords_Flag;
|
||||
}
|
||||
|
||||
@ -201,7 +215,7 @@ SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl,
|
||||
// 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 @@ SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl,
|
||||
// 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 @@ SkRuntimeEffect::Result SkRuntimeEffect::Make(SkString sksl,
|
||||
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
|
||||
|
@ -40,8 +40,13 @@ public:
|
||||
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 @@ public:
|
||||
}
|
||||
|
||||
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 @@ public:
|
||||
.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 @@ public:
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,8 @@ static bool is_sample_call_to_fp(const FunctionCall& fc, const Variable& fp) {
|
||||
// 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 @@ public:
|
||||
protected:
|
||||
const Context& fContext;
|
||||
const Variable& fFP;
|
||||
const bool fWritesToSampleCoords;
|
||||
SampleUsage fUsage;
|
||||
|
||||
bool visitExpression(const Expression& e) override {
|
||||
@ -97,7 +98,18 @@ protected:
|
||||
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 @@ public:
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,14 @@ enum class VariableRefKind : int8_t;
|
||||
* 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);
|
||||
|
||||
|
@ -199,6 +199,8 @@ struct Program {
|
||||
return result;
|
||||
}
|
||||
|
||||
const ProgramUsage* usage() const { return fUsage.get(); }
|
||||
|
||||
std::unique_ptr<String> fSource;
|
||||
std::unique_ptr<ProgramConfig> fConfig;
|
||||
std::shared_ptr<Context> fContext;
|
||||
|
@ -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 @@ DEF_TEST(SkRuntimeColorFilterFlags, r) {
|
||||
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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user