Support function calls in SkSL-to-SkVM

Bug: skia:10680
Change-Id: I8697bdc157d250f3c390c7f49074318aa8c7bdab
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/351918
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Mike Klein <mtklein@google.com>
This commit is contained in:
Brian Osman 2021-01-07 14:38:08 -05:00 committed by Skia Commit-Bot
parent 6bf6963198
commit 54515b7b2b
3 changed files with 113 additions and 41 deletions

View File

@ -78,16 +78,9 @@ private:
fCompiler = new SkSL::Compiler(fCaps.get());
// Using an inline threshold of zero would stop all inlining, and cause us to re-emit
// SkSL that is nearly identical to what was ingested. That would be in the spirit of
// applying no workarounds, but causes problems (today). On the CPU backend, we only
// compile the user SkSL once, then emit directly to skvm. The CPU backend doesn't
// support function calls, so some tests only work because of inlining. This needs to
// be addressed robustly - by adding function call support and/or forcing inlining,
// but for now, we use defaults that let the majority of our test cases work on all
// backends. (Note that there are other control flow constructs that don't work on the
// CPU backend, this is a special case of a more general problem.) skbug.com/10680
fInlineThreshold = SkSL::Program::Settings().fInlineThreshold;
// Using an inline threshold of zero stops all inlining, and causes us to re-emit SkSL
// that is nearly identical to what was ingested.
fInlineThreshold = 0;
}
SkSL::ShaderCapsPointer fCaps;

View File

@ -74,6 +74,8 @@ struct Value {
ValRef operator[](int i) { return fVals[i]; }
skvm::Val operator[](int i) const { return fVals[i]; }
SkSpan<skvm::Val> asSpan() { return fVals; }
private:
SkSTArray<4, skvm::Val, true> fVals;
};
@ -209,7 +211,12 @@ private:
return result;
}
skvm::I32 mask() { return fMask; }
skvm::I32 mask() {
// As we encounter (possibly conditional) return statements, fReturned is updated to store
// the lanes that have already returned. For the remainder of the current function, those
// lanes should be disabled.
return fMask & ~currentFunction().fReturned;
}
Value writeExpression(const Expression& expr);
Value writeBinaryExpression(const BinaryExpression& b);
@ -248,12 +255,18 @@ private:
std::unordered_map<const Variable*, Slot> fVariableMap;
std::vector<skvm::Val> fSlots;
// Conditional execution mask (changes are managed by AutoMask, and tied to control-flow scopes)
skvm::I32 fMask;
//
// State that's local to the generation of a single function:
//
SkSpan<skvm::Val> fReturnValue;
skvm::I32 fMask;
skvm::I32 fReturned;
struct Function {
const SkSpan<skvm::Val> fReturnValue;
skvm::I32 fReturned;
};
std::vector<Function> fFunctionStack;
Function& currentFunction() { return fFunctionStack.back(); }
class AutoMask {
public:
@ -362,8 +375,7 @@ SkVMGenerator::SkVMGenerator(const Program& program,
{ "sample", Intrinsic::kSample },
} {
fMask = fBuilder->splat(0xffff'ffff);
fReturned = fBuilder->splat(0);
fMask = fBuilder->splat(0xffff'ffff);
// Now, add storage for each global variable (including uniforms) to fSlots, and entries in
// fVariableMap to remember where every variable is stored.
@ -420,11 +432,10 @@ SkVMGenerator::SkVMGenerator(const Program& program,
void SkVMGenerator::writeFunction(const FunctionDefinition& function,
SkSpan<skvm::Val> arguments,
SkSpan<skvm::Val> outReturn) {
const FunctionDeclaration& decl = function.declaration();
SkASSERT(slot_count(decl.returnType()) == outReturn.size());
fReturnValue = outReturn;
SkASSERT(slot_count(decl.returnType()) == fReturnValue.size());
fFunctionStack.push_back({outReturn, /*returned=*/fBuilder->splat(0)});
// For all parameters, copy incoming argument IDs to our vector of (all) variable IDs
size_t argIdx = 0;
@ -455,6 +466,8 @@ void SkVMGenerator::writeFunction(const FunctionDefinition& function,
argIdx += nslots;
}
SkASSERT(argIdx == arguments.size());
fFunctionStack.pop_back();
}
SkVMGenerator::Slot SkVMGenerator::getSlot(const Variable& v) {
@ -1122,13 +1135,53 @@ Value SkVMGenerator::writeIntrinsicCall(const FunctionCall& c) {
}
Value SkVMGenerator::writeFunctionCall(const FunctionCall& f) {
// TODO: Support calling other functions (by recursively generating their programs, eg inlining)
if (f.function().isBuiltin()) {
if (f.function().isBuiltin() && !f.function().definition()) {
return this->writeIntrinsicCall(f);
}
SkDEBUGFAIL("Function calls not supported yet");
return {};
const FunctionDeclaration& decl = f.function();
// Evaluate all arguments, gather the results into a contiguous list of IDs
std::vector<skvm::Val> argVals;
for (const auto& arg : f.arguments()) {
Value v = this->writeExpression(*arg);
for (size_t i = 0; i < v.slots(); ++i) {
argVals.push_back(v[i]);
}
}
// Create storage for the return value
size_t nslots = slot_count(f.type());
Value result(nslots);
for (size_t i = 0; i < nslots; ++i) {
result[i] = fBuilder->splat(0.0f);
}
{
// This AutoMask merges currentFunction().fReturned into fMask. Lanes that conditionally
// returned in the current function would otherwise resume execution within the child.
AutoMask m(this, ~currentFunction().fReturned);
this->writeFunction(*f.function().definition(), argVals, result.asSpan());
}
// Propagate new values of any 'out' params back to the original arguments
const std::unique_ptr<Expression>* argIter = f.arguments().begin();
size_t valIdx = 0;
for (const Variable* p : decl.parameters()) {
size_t nslots = slot_count(p->type());
if (p->modifiers().fFlags & Modifiers::kOut_Flag) {
Value v(nslots);
for (size_t i = 0; i < nslots; ++i) {
v[i] = argVals[valIdx + i];
}
const std::unique_ptr<Expression>& arg = *argIter;
this->writeStore(*arg, v);
}
valIdx += nslots;
argIter++;
}
return result;
}
Value SkVMGenerator::writePrefixExpression(const PrefixExpression& p) {
@ -1307,18 +1360,19 @@ void SkVMGenerator::writeIfStatement(const IfStatement& i) {
}
void SkVMGenerator::writeReturnStatement(const ReturnStatement& r) {
// TODO: Can we suppress other side effects for lanes that have returned? fMask needs to
// fold in knowledge of conditional returns earlier in the function.
skvm::I32 returnsHere = bit_clear(this->mask(), fReturned);
skvm::I32 returnsHere = this->mask();
// TODO: returns with no expression
Value val = this->writeExpression(*r.expression());
if (r.expression()) {
Value val = this->writeExpression(*r.expression());
for (size_t i = 0; i < val.slots(); ++i) {
fReturnValue[i] = select(returnsHere, f32(val[i]), f32(fReturnValue[i])).id;
int i = 0;
for (skvm::Val& slot : currentFunction().fReturnValue) {
slot = select(returnsHere, f32(val[i]), f32(slot)).id;
i++;
}
}
fReturned |= returnsHere;
currentFunction().fReturned |= returnsHere;
}
void SkVMGenerator::writeVarDeclaration(const VarDeclaration& decl) {

View File

@ -20,6 +20,10 @@ struct ProgramBuilder {
ProgramBuilder(skiatest::Reporter* r, const char* src)
: fCaps(GrContextOptions{}), fCompiler(&fCaps) {
SkSL::Program::Settings settings;
// The SkSL inliner is well tested in other contexts. Here, we disable inlining entirely,
// to stress-test the VM generator's handling of function calls with varying signatures.
settings.fInlineThreshold = 0;
fProgram = fCompiler.convertProgram(SkSL::Program::kGeneric_Kind, SkSL::String(src),
settings);
if (!fProgram) {
@ -290,15 +294,12 @@ DEF_TEST(SkSLInterpreterAnd, r) {
"color = half4(color.a); }", 1, 1, 0, 3, 1, 1, 0, 3);
test(r, "void main(inout half4 color) { if (color.r > color.g && color.g > color.b) "
"color = half4(color.a); }", 2, 1, 1, 3, 2, 1, 1, 3);
// TODO: SkVM function call support
test(r, "int global; bool update() { global = 123; return true; }"
"void main(inout half4 color) { global = 0; if (color.r > color.g && update()) "
"color = half4(color.a); color.a = global; }", 2, 1, 1, 3, 3, 3, 3, 123,
/*testWithSkVM=*/false);
"color = half4(color.a); color.a = global; }", 2, 1, 1, 3, 3, 3, 3, 123);
test(r, "int global; bool update() { global = 123; return true; }"
"void main(inout half4 color) { global = 0; if (color.r > color.g && update()) "
"color = half4(color.a); color.a = global; }", 1, 1, 1, 3, 1, 1, 1, 0,
/*testWithSkVM=*/false);
"color = half4(color.a); color.a = global; }", 1, 1, 1, 3, 1, 1, 1, 0);
}
DEF_TEST(SkSLInterpreterOr, r) {
@ -308,15 +309,12 @@ DEF_TEST(SkSLInterpreterOr, r) {
"color = half4(color.a); }", 1, 1, 0, 3, 3, 3, 3, 3);
test(r, "void main(inout half4 color) { if (color.r > color.g || color.g > color.b) "
"color = half4(color.a); }", 1, 1, 1, 3, 1, 1, 1, 3);
// TODO: SkVM function call support
test(r, "int global; bool update() { global = 123; return true; }"
"void main(inout half4 color) { global = 0; if (color.r > color.g || update()) "
"color = half4(color.a); color.a = global; }", 1, 1, 1, 3, 3, 3, 3, 123,
/*testWithSkVM=*/false);
"color = half4(color.a); color.a = global; }", 1, 1, 1, 3, 3, 3, 3, 123);
test(r, "int global; bool update() { global = 123; return true; }"
"void main(inout half4 color) { global = 0; if (color.r > color.g || update()) "
"color = half4(color.a); color.a = global; }", 2, 1, 1, 3, 3, 3, 3, 0,
/*testWithSkVM=*/false);
"color = half4(color.a); color.a = global; }", 2, 1, 1, 3, 3, 3, 3, 0);
}
DEF_TEST(SkSLInterpreterMatrix, r) {
@ -738,6 +736,33 @@ DEF_TEST(SkSLInterpreterRestrictFunctionCalls, r) {
"{ for (int i = 0; i < 1; i++) { if (x > 2) { return x; } } return 0; }");
}
DEF_TEST(SkSLInterpreterReturnThenCall, r) {
// Test that early returns disable execution in subsequently called functions
const char* src = R"(
float y;
void inc () { ++y; }
void maybe_inc() { if (y < 0) return; inc(); }
void main(inout float x) { y = x; maybe_inc(); x = y; }
)";
ProgramBuilder program(r, src);
const SkSL::FunctionDefinition* main = SkSL::Program_GetFunction(*program, "main");
REPORTER_ASSERT(r, main);
skvm::Builder b;
SkSL::ProgramToSkVM(*program, *main, &b);
skvm::Program p = b.done();
float xs[] = { -2.0f, 0.0f, 3.0f, -1.0f };
const void* uniforms = nullptr;
p.eval(4, uniforms, xs);
REPORTER_ASSERT(r, xs[0] == -2.0f);
REPORTER_ASSERT(r, xs[1] == 1.0f);
REPORTER_ASSERT(r, xs[2] == 4.0f);
REPORTER_ASSERT(r, xs[3] == -1.0f);
}
DEF_TEST(SkSLInterpreterEarlyReturn, r) {
// Unlike returns in loops, returns in conditionals should work.
const char* src = "float main(float x, float y) { if (x < y) { return x; } return y; }";