blob: fa9258ec027ff6027fac6534e4f2125b4c547ba7 [file] [log] [blame]
/*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/sksl/SkSLCompiler.h"
#include "include/private/base/SkDebug.h"
#include "src/core/SkTraceEvent.h"
#include "src/sksl/SkSLAnalysis.h"
#include "src/sksl/SkSLContext.h"
#include "src/sksl/SkSLDefines.h"
#include "src/sksl/SkSLInliner.h"
#include "src/sksl/SkSLModuleLoader.h"
#include "src/sksl/SkSLParser.h"
#include "src/sksl/SkSLPool.h"
#include "src/sksl/SkSLProgramKind.h"
#include "src/sksl/SkSLProgramSettings.h"
#include "src/sksl/analysis/SkSLProgramUsage.h"
#include "src/sksl/ir/SkSLProgram.h"
#include "src/sksl/ir/SkSLSymbolTable.h" // IWYU pragma: keep
#include "src/sksl/transform/SkSLTransform.h"
#include <cstdint>
#include <memory>
#include <utility>
#if defined(SKSL_STANDALONE)
#include <fstream>
#endif
namespace SkSL {
// These flags allow tools like Viewer or Nanobench to override the compiler's ProgramSettings.
Compiler::OverrideFlag Compiler::sOptimizer = OverrideFlag::kDefault;
Compiler::OverrideFlag Compiler::sInliner = OverrideFlag::kDefault;
class AutoProgramConfig {
public:
AutoProgramConfig(Context& context, ProgramConfig* config)
: fContext(context)
, fOldConfig(context.fConfig) {
fContext.fConfig = config;
}
~AutoProgramConfig() {
fContext.fConfig = fOldConfig;
}
Context& fContext;
ProgramConfig* fOldConfig;
};
Compiler::Compiler() : fErrorReporter(this) {
auto moduleLoader = ModuleLoader::Get();
fContext = std::make_shared<Context>(moduleLoader.builtinTypes(), fErrorReporter);
}
Compiler::~Compiler() {}
const Module* Compiler::moduleForProgramKind(ProgramKind kind) {
auto m = ModuleLoader::Get();
switch (kind) {
case ProgramKind::kFragment: return m.loadFragmentModule(this);
case ProgramKind::kVertex: return m.loadVertexModule(this);
case ProgramKind::kCompute: return m.loadComputeModule(this);
case ProgramKind::kGraphiteFragment: return m.loadGraphiteFragmentModule(this);
case ProgramKind::kGraphiteVertex: return m.loadGraphiteVertexModule(this);
case ProgramKind::kGraphiteFragmentES2: return m.loadGraphiteFragmentES2Module(this);
case ProgramKind::kGraphiteVertexES2: return m.loadGraphiteVertexES2Module(this);
case ProgramKind::kPrivateRuntimeShader: return m.loadPrivateRTShaderModule(this);
case ProgramKind::kRuntimeColorFilter:
case ProgramKind::kRuntimeShader:
case ProgramKind::kRuntimeBlender:
case ProgramKind::kPrivateRuntimeColorFilter:
case ProgramKind::kPrivateRuntimeBlender:
case ProgramKind::kMeshVertex:
case ProgramKind::kMeshFragment: return m.loadPublicModule(this);
}
SkUNREACHABLE;
}
void Compiler::FinalizeSettings(ProgramSettings* settings, ProgramKind kind) {
// Honor our optimization-override flags.
switch (sOptimizer) {
case OverrideFlag::kDefault:
break;
case OverrideFlag::kOff:
settings->fOptimize = false;
break;
case OverrideFlag::kOn:
settings->fOptimize = true;
break;
}
switch (sInliner) {
case OverrideFlag::kDefault:
break;
case OverrideFlag::kOff:
settings->fInlineThreshold = 0;
break;
case OverrideFlag::kOn:
if (settings->fInlineThreshold == 0) {
settings->fInlineThreshold = kDefaultInlineThreshold;
}
break;
}
// Disable optimization settings that depend on a parent setting which has been disabled.
settings->fInlineThreshold *= (int)settings->fOptimize;
settings->fRemoveDeadFunctions &= settings->fOptimize;
settings->fRemoveDeadVariables &= settings->fOptimize;
// Runtime effects always allow narrowing conversions.
if (ProgramConfig::IsRuntimeEffect(kind)) {
settings->fAllowNarrowingConversions = true;
}
}
void Compiler::initializeContext(const SkSL::Module* module,
ProgramKind kind,
ProgramSettings settings,
std::string_view source,
bool isModule) {
SkASSERT(!fPool);
SkASSERT(!fConfig);
SkASSERT(!fContext->fSymbolTable);
SkASSERT(!fContext->fConfig);
SkASSERT(!fContext->fModule);
// Start the ErrorReporter with a clean slate.
this->resetErrors();
fConfig = std::make_unique<ProgramConfig>();
fConfig->fIsBuiltinCode = isModule;
fConfig->fSettings = settings;
fConfig->fKind = kind;
// Make sure the passed-in settings are valid.
FinalizeSettings(&fConfig->fSettings, kind);
if (settings.fUseMemoryPool) {
fPool = Pool::Create();
fPool->attachToThread();
}
fContext->fConfig = fConfig.get();
fContext->fModule = module;
fContext->fErrors->setSource(source);
// Set up a clean symbol table atop the parent module's symbols.
fGlobalSymbols = std::make_unique<SymbolTable>(module->fSymbols.get(), isModule);
fGlobalSymbols->markModuleBoundary();
fContext->fSymbolTable = fGlobalSymbols.get();
}
void Compiler::cleanupContext() {
// Clear out the fields we initialized above.
fContext->fConfig = nullptr;
fContext->fModule = nullptr;
fContext->fErrors->setSource(std::string_view());
fContext->fSymbolTable = nullptr;
fConfig = nullptr;
fGlobalSymbols = nullptr;
if (fPool) {
fPool->detachFromThread();
fPool = nullptr;
}
}
std::unique_ptr<Module> Compiler::compileModule(ProgramKind kind,
const char* moduleName,
std::string moduleSource,
const Module* parentModule,
bool shouldInline) {
SkASSERT(parentModule);
SkASSERT(!moduleSource.empty());
SkASSERT(this->errorCount() == 0);
// Wrap the program source in a pointer so it is guaranteed to be stable across moves.
auto sourcePtr = std::make_unique<std::string>(std::move(moduleSource));
// Compile the module from source, using default program settings (but no memory pooling).
ProgramSettings settings;
settings.fUseMemoryPool = false;
this->initializeContext(parentModule, kind, settings, *sourcePtr, /*isModule=*/true);
std::unique_ptr<Module> module = SkSL::Parser(this, settings, kind, std::move(sourcePtr))
.moduleInheritingFrom(parentModule);
this->cleanupContext();
if (this->errorCount() != 0) {
SkDebugf("Unexpected errors compiling %s:\n\n%s\n", moduleName, this->errorText().c_str());
return nullptr;
}
if (shouldInline) {
this->optimizeModuleAfterLoading(kind, *module);
}
return module;
}
std::unique_ptr<Program> Compiler::convertProgram(ProgramKind kind,
std::string programSource,
const ProgramSettings& settings) {
TRACE_EVENT0("skia.shaders", "SkSL::Compiler::convertProgram");
// Wrap the program source in a pointer so it is guaranteed to be stable across moves.
auto sourcePtr = std::make_unique<std::string>(std::move(programSource));
// Load the module used by this ProgramKind.
const SkSL::Module* module = this->moduleForProgramKind(kind);
this->initializeContext(module, kind, settings, *sourcePtr, /*isModule=*/false);
std::unique_ptr<Program> program = SkSL::Parser(this, settings, kind, std::move(sourcePtr))
.programInheritingFrom(module);
this->cleanupContext();
return program;
}
std::unique_ptr<SkSL::Program> Compiler::releaseProgram(
std::unique_ptr<std::string> source,
std::vector<std::unique_ptr<SkSL::ProgramElement>> programElements) {
Pool* pool = fPool.get();
auto result = std::make_unique<SkSL::Program>(std::move(source),
std::move(fConfig),
fContext,
std::move(programElements),
std::move(fGlobalSymbols),
std::move(fPool));
fContext->fSymbolTable = nullptr;
bool success = this->finalize(*result) &&
this->optimize(*result);
if (pool) {
pool->detachFromThread();
}
return success ? std::move(result) : nullptr;
}
bool Compiler::optimizeModuleBeforeMinifying(ProgramKind kind, Module& module, bool shrinkSymbols) {
SkASSERT(this->errorCount() == 0);
auto m = SkSL::ModuleLoader::Get();
// Create a temporary program configuration with default settings.
ProgramConfig config;
config.fIsBuiltinCode = true;
config.fKind = kind;
AutoProgramConfig autoConfig(this->context(), &config);
std::unique_ptr<ProgramUsage> usage = Analysis::GetUsage(module);
if (shrinkSymbols) {
// Assign shorter names to symbols as long as it won't change the external meaning of the
// code.
Transform::RenamePrivateSymbols(this->context(), module, usage.get(), kind);
// Replace constant variables with their literal values to save space.
Transform::ReplaceConstVarsWithLiterals(module, usage.get());
}
// Remove any unreachable code.
Transform::EliminateUnreachableCode(module, usage.get());
// We can only remove dead functions from runtime shaders, since runtime-effect helper functions
// are isolated from other parts of the program. In a module, an unreferenced function is
// intended to be called by the code that includes the module.
if (kind == ProgramKind::kRuntimeShader) {
while (Transform::EliminateDeadFunctions(this->context(), module, usage.get())) {
// Removing dead functions may cause more functions to become unreferenced. Try again.
}
}
while (Transform::EliminateDeadLocalVariables(this->context(), module, usage.get())) {
// Removing dead variables may cause more variables to become unreferenced. Try again.
}
// Runtime shaders are isolated from other parts of the program via name mangling, so we can
// eliminate public globals if they aren't referenced. Otherwise, we only eliminate private
// globals (prefixed with `$`) to avoid changing the meaning of the module code.
bool onlyPrivateGlobals = !ProgramConfig::IsRuntimeEffect(kind);
while (Transform::EliminateDeadGlobalVariables(this->context(), module, usage.get(),
onlyPrivateGlobals)) {
// Repeat until no changes occur.
}
// We eliminate empty statements to avoid runs of `;;;;;;` caused by the previous passes.
SkSL::Transform::EliminateEmptyStatements(module);
// We can eliminate `{}` around single-statement blocks.
SkSL::Transform::EliminateUnnecessaryBraces(module);
// Make sure that program usage is still correct after the optimization pass is complete.
SkASSERT(*usage == *Analysis::GetUsage(module));
return this->errorCount() == 0;
}
bool Compiler::optimizeModuleAfterLoading(ProgramKind kind, Module& module) {
SkASSERT(this->errorCount() == 0);
#ifndef SK_ENABLE_OPTIMIZE_SIZE
// Create a temporary program configuration with default settings.
ProgramConfig config;
config.fIsBuiltinCode = true;
config.fKind = kind;
AutoProgramConfig autoConfig(this->context(), &config);
std::unique_ptr<ProgramUsage> usage = Analysis::GetUsage(module);
// Perform inline-candidate analysis and inline any functions deemed suitable.
Inliner inliner(fContext.get());
while (this->errorCount() == 0) {
if (!this->runInliner(&inliner, module.fElements, module.fSymbols.get(), usage.get())) {
break;
}
}
// Make sure that program usage is still correct after the optimization pass is complete.
SkASSERT(*usage == *Analysis::GetUsage(module));
#endif
return this->errorCount() == 0;
}
bool Compiler::optimize(Program& program) {
// The optimizer only needs to run when it is enabled.
if (!program.fConfig->fSettings.fOptimize) {
return true;
}
SkASSERT(!this->errorCount());
if (this->errorCount() == 0) {
#ifndef SK_ENABLE_OPTIMIZE_SIZE
// Run the inliner only once; it is expensive! Multiple passes can occasionally shake out
// more wins, but it's diminishing returns.
Inliner inliner(fContext.get());
this->runInliner(&inliner, program.fOwnedElements, program.fSymbols.get(),
program.fUsage.get());
#endif
// Unreachable code can confuse some drivers, so it's worth removing. (skia:12012)
Transform::EliminateUnreachableCode(program);
while (Transform::EliminateDeadFunctions(program)) {
// Removing dead functions may cause more functions to become unreferenced. Try again.
}
while (Transform::EliminateDeadLocalVariables(program)) {
// Removing dead variables may cause more variables to become unreferenced. Try again.
}
while (Transform::EliminateDeadGlobalVariables(program)) {
// Repeat until no changes occur.
}
// Make sure that program usage is still correct after the optimization pass is complete.
SkASSERT(*program.usage() == *Analysis::GetUsage(program));
// Make sure that variables are still declared in the correct symbol tables.
SkDEBUGCODE(Analysis::CheckSymbolTableCorrectness(program));
}
return this->errorCount() == 0;
}
void Compiler::runInliner(Program& program) {
#ifndef SK_ENABLE_OPTIMIZE_SIZE
AutoProgramConfig autoConfig(this->context(), program.fConfig.get());
Inliner inliner(fContext.get());
this->runInliner(&inliner, program.fOwnedElements, program.fSymbols.get(),
program.fUsage.get());
#endif
}
bool Compiler::runInliner(Inliner* inliner,
const std::vector<std::unique_ptr<ProgramElement>>& elements,
SymbolTable* symbols,
ProgramUsage* usage) {
#ifdef SK_ENABLE_OPTIMIZE_SIZE
return true;
#else
// The program's SymbolTable was taken out of the context when the program was bundled, but
// the inliner creates IR objects which may expect the context to hold a valid SymbolTable.
SkASSERT(!fContext->fSymbolTable);
fContext->fSymbolTable = symbols;
bool result = inliner->analyze(elements, symbols, usage);
fContext->fSymbolTable = nullptr;
return result;
#endif
}
bool Compiler::finalize(Program& program) {
// Copy all referenced built-in functions into the Program.
Transform::FindAndDeclareBuiltinFunctions(program);
// Variables defined in modules need their declaring elements added to the program.
Transform::FindAndDeclareBuiltinVariables(program);
// Structs from module code need to be added to the program's shared elements.
Transform::FindAndDeclareBuiltinStructs(program);
// Do one last correctness-check pass. This looks for dangling FunctionReference/TypeReference
// expressions, and reports them as errors.
Analysis::DoFinalizationChecks(program);
if (fContext->fConfig->strictES2Mode() && this->errorCount() == 0) {
// Enforce Appendix A, Section 5 of the GLSL ES 1.00 spec -- Indexing. This logic assumes
// that all loops meet the criteria of Section 4, and if they don't, could crash.
for (const auto& pe : program.fOwnedElements) {
Analysis::ValidateIndexingForES2(*pe, this->errorReporter());
}
}
if (this->errorCount() == 0) {
bool enforceSizeLimit = ProgramConfig::IsRuntimeEffect(program.fConfig->fKind);
Analysis::CheckProgramStructure(program, enforceSizeLimit);
// Make sure that variables are declared in the symbol tables that immediately enclose them.
SkDEBUGCODE(Analysis::CheckSymbolTableCorrectness(program));
}
// Make sure that program usage is still correct after finalization is complete.
SkASSERT(*program.usage() == *Analysis::GetUsage(program));
return this->errorCount() == 0;
}
void Compiler::handleError(std::string_view msg, Position pos) {
fErrorText += "error: ";
bool printLocation = false;
std::string_view src = this->errorReporter().source();
int line = -1;
if (pos.valid()) {
line = pos.line(src);
printLocation = pos.startOffset() < (int)src.length();
fErrorText += std::to_string(line) + ": ";
}
fErrorText += std::string(msg) + "\n";
if (printLocation) {
const int kMaxSurroundingChars = 100;
// Find the beginning of the line.
int lineStart = pos.startOffset();
while (lineStart > 0) {
if (src[lineStart - 1] == '\n') {
break;
}
--lineStart;
}
// We don't want to show more than 100 characters surrounding the error, so push the line
// start forward and add a leading ellipsis if there would be more than this.
std::string lineText;
std::string caretText;
if ((pos.startOffset() - lineStart) > kMaxSurroundingChars) {
lineStart = pos.startOffset() - kMaxSurroundingChars;
lineText = "...";
caretText = " ";
}
// Echo the line. Again, we don't want to show more than 100 characters after the end of the
// error, so truncate with a trailing ellipsis if needed.
const char* lineSuffix = "...\n";
int lineStop = pos.endOffset() + kMaxSurroundingChars;
if (lineStop >= (int)src.length()) {
lineStop = src.length() - 1;
lineSuffix = "\n"; // no ellipsis if we reach end-of-file
}
for (int i = lineStart; i < lineStop; ++i) {
char c = src[i];
if (c == '\n') {
lineSuffix = "\n"; // no ellipsis if we reach end-of-line
break;
}
switch (c) {
case '\t': lineText += " "; break;
case '\0': lineText += " "; break;
default: lineText += src[i]; break;
}
}
fErrorText += lineText + lineSuffix;
// print the carets underneath it, pointing to the range in question
for (int i = lineStart; i < (int)src.length(); i++) {
if (i >= pos.endOffset()) {
break;
}
switch (src[i]) {
case '\t':
caretText += (i >= pos.startOffset()) ? "^^^^" : " ";
break;
case '\n':
SkASSERT(i >= pos.startOffset());
// use an ellipsis if the error continues past the end of the line
caretText += (pos.endOffset() > i + 1) ? "..." : "^";
i = src.length();
break;
default:
caretText += (i >= pos.startOffset()) ? '^' : ' ';
break;
}
}
fErrorText += caretText + '\n';
}
}
std::string Compiler::errorText(bool showCount) {
if (showCount) {
this->writeErrorCount();
}
std::string result = fErrorText;
this->resetErrors();
return result;
}
void Compiler::writeErrorCount() {
int count = this->errorCount();
if (count) {
fErrorText += std::to_string(count) +
((count == 1) ? " error\n" : " errors\n");
}
}
} // namespace SkSL