/* * 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/SkSLDefines.h" #include "include/private/SkSLIRNode.h" #include "include/private/SkSLProgramKind.h" #include "include/private/SkSLSymbol.h" #include "include/private/base/SkDebug.h" #include "include/sksl/DSLCore.h" #include "include/sksl/DSLModifiers.h" #include "include/sksl/DSLType.h" #include "src/core/SkTraceEvent.h" #include "src/sksl/SkSLAnalysis.h" #include "src/sksl/SkSLContext.h" #include "src/sksl/SkSLInliner.h" #include "src/sksl/SkSLModuleLoader.h" #include "src/sksl/SkSLOutputStream.h" #include "src/sksl/SkSLParser.h" #include "src/sksl/SkSLProgramSettings.h" #include "src/sksl/SkSLStringStream.h" #include "src/sksl/analysis/SkSLProgramUsage.h" #include "src/sksl/ir/SkSLExpression.h" #include "src/sksl/ir/SkSLField.h" #include "src/sksl/ir/SkSLFieldAccess.h" #include "src/sksl/ir/SkSLFunctionDeclaration.h" #include "src/sksl/ir/SkSLFunctionReference.h" #include "src/sksl/ir/SkSLProgram.h" #include "src/sksl/ir/SkSLSymbolTable.h" // IWYU pragma: keep #include "src/sksl/ir/SkSLTypeReference.h" #include "src/sksl/ir/SkSLVariable.h" #include "src/sksl/ir/SkSLVariableReference.h" #include "src/sksl/transform/SkSLTransform.h" #include #include #include #include #if defined(SKSL_STANDALONE) #include #endif #if defined(SKSL_STANDALONE) || defined(SK_GANESH) || defined(SK_GRAPHITE) #include "src/sksl/codegen/SkSLGLSLCodeGenerator.h" #include "src/sksl/codegen/SkSLMetalCodeGenerator.h" #include "src/sksl/codegen/SkSLSPIRVCodeGenerator.h" #include "src/sksl/codegen/SkSLSPIRVtoHLSL.h" #include "src/sksl/codegen/SkSLWGSLCodeGenerator.h" #endif #ifdef SK_ENABLE_SPIRV_VALIDATION #include "spirv-tools/libspirv.hpp" #endif #ifdef SK_ENABLE_WGSL_VALIDATION #include "tint/tint.h" #endif namespace SkSL { class ModifiersPool; // 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; using RefKind = VariableReference::RefKind; class AutoSource { public: AutoSource(Compiler* compiler, std::string_view source) : fCompiler(compiler) { SkASSERT(!fCompiler->errorReporter().source().data()); fCompiler->errorReporter().setSource(source); } ~AutoSource() { fCompiler->errorReporter().setSource(std::string_view()); } Compiler* fCompiler; }; class AutoProgramConfig { public: AutoProgramConfig(Context& context, ProgramConfig* config) : fContext(context) , fOldConfig(context.fConfig) { fContext.fConfig = config; } ~AutoProgramConfig() { fContext.fConfig = fOldConfig; } Context& fContext; ProgramConfig* fOldConfig; }; class AutoShaderCaps { public: AutoShaderCaps(std::shared_ptr& context, const ShaderCaps* caps) : fContext(context.get()) , fOldCaps(fContext->fCaps) { fContext->fCaps = caps; } ~AutoShaderCaps() { fContext->fCaps = fOldCaps; } Context* fContext; const ShaderCaps* fOldCaps; }; class AutoModifiersPool { public: AutoModifiersPool(std::shared_ptr& context, ModifiersPool* modifiersPool) : fContext(context.get()) { SkASSERT(!fContext->fModifiersPool); fContext->fModifiersPool = modifiersPool; } ~AutoModifiersPool() { fContext->fModifiersPool = nullptr; } Context* fContext; }; Compiler::Compiler(const ShaderCaps* caps) : fErrorReporter(this), fCaps(caps) { SkASSERT(caps); auto moduleLoader = ModuleLoader::Get(); fContext = std::make_shared(moduleLoader.builtinTypes(), /*caps=*/nullptr, fErrorReporter); } Compiler::~Compiler() {} const Module* Compiler::moduleForProgramKind(ProgramKind kind) { auto m = ModuleLoader::Get(); switch (kind) { case ProgramKind::kVertex: return m.loadVertexModule(this); case ProgramKind::kFragment: return m.loadFragmentModule(this); case ProgramKind::kCompute: return m.loadComputeModule(this); case ProgramKind::kGraphiteVertex: return m.loadGraphiteVertexModule(this); case ProgramKind::kGraphiteFragment: return m.loadGraphiteFragmentModule(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: case ProgramKind::kGeneric: 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; if (kind == ProgramKind::kGeneric) { // For "generic" interpreter programs, leave all functions intact. (The SkVM API supports // calling any function, not just 'main'). settings->fRemoveDeadFunctions = false; } // Runtime effects always allow narrowing conversions. if (ProgramConfig::IsRuntimeEffect(kind)) { settings->fAllowNarrowingConversions = true; } } std::unique_ptr Compiler::compileModule(ProgramKind kind, const char* moduleName, std::string moduleSource, const Module* parent, ModifiersPool& modifiersPool, bool shouldInline) { SkASSERT(parent); SkASSERT(!moduleSource.empty()); SkASSERT(this->errorCount() == 0); // Modules are shared and cannot rely on shader caps. AutoShaderCaps autoCaps(fContext, nullptr); AutoModifiersPool autoPool(fContext, &modifiersPool); // Compile the module from source, using default program settings. ProgramSettings settings; FinalizeSettings(&settings, kind); SkSL::Parser parser{this, settings, kind, std::move(moduleSource)}; std::unique_ptr module = parser.moduleInheritingFrom(parent); 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 Compiler::convertProgram(ProgramKind kind, std::string text, ProgramSettings settings) { TRACE_EVENT0("skia.shaders", "SkSL::Compiler::convertProgram"); // Make sure the passed-in settings are valid. FinalizeSettings(&settings, kind); // Put the ShaderCaps into the context while compiling a program. AutoShaderCaps autoCaps(fContext, fCaps); this->resetErrors(); return Parser(this, settings, kind, std::move(text)).program(); } std::unique_ptr Compiler::convertIdentifier(Position pos, std::string_view name) { const Symbol* result = fSymbolTable->find(name); if (!result) { this->errorReporter().error(pos, "unknown identifier '" + std::string(name) + "'"); return nullptr; } switch (result->kind()) { case Symbol::Kind::kFunctionDeclaration: { return std::make_unique(*fContext, pos, &result->as()); } case Symbol::Kind::kVariable: { const Variable* var = &result->as(); // default to kRead_RefKind; this will be corrected later if the variable is written to return VariableReference::Make(pos, var, VariableReference::RefKind::kRead); } case Symbol::Kind::kField: { const Field* field = &result->as(); auto base = VariableReference::Make(pos, &field->owner(), VariableReference::RefKind::kRead); return FieldAccess::Make(*fContext, pos, std::move(base), field->fieldIndex(), FieldAccess::OwnerKind::kAnonymousInterfaceBlock); } case Symbol::Kind::kType: { // go through DSLType so we report errors on private types dsl::DSLModifiers modifiers; dsl::DSLType dslType(result->name(), &modifiers, pos); return TypeReference::Convert(*fContext, pos, &dslType.skslType()); } default: SK_ABORT("unsupported symbol type %d\n", (int) result->kind()); } } bool Compiler::optimizeModuleBeforeMinifying(ProgramKind kind, Module& module) { 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); AutoModifiersPool autoPool(fContext, &m.coreModifiers()); std::unique_ptr usage = Analysis::GetUsage(module); // 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); // 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 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, 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; } AutoShaderCaps autoCaps(fContext, fCaps); 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, 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)); } return this->errorCount() == 0; } bool Compiler::runInliner(Inliner* inliner, const std::vector>& elements, std::shared_ptr symbols, ProgramUsage* usage) { #ifdef SK_ENABLE_OPTIMIZE_SIZE return true; #else // The program's SymbolTable was taken out of fSymbolTable when the program was bundled, but // the inliner relies (indirectly) on having a valid SymbolTable. // In particular, inlining can turn a non-optimizable expression like `normalize(myVec)` into // `normalize(vec2(7))`, which is now optimizable. The optimizer can use DSL to simplify this // expression--e.g., in the case of normalize, using DSL's Length(). The DSL relies on // convertIdentifier() to look up `length`. convertIdentifier() needs a valid symbol table to // find the declaration of `length`. To allow this chain of events to succeed, we re-insert the // program's symbol table temporarily. SkASSERT(!fSymbolTable); fSymbolTable = symbols; bool result = inliner->analyze(elements, symbols, usage); fSymbolTable = nullptr; return result; #endif } bool Compiler::finalize(Program& program) { AutoShaderCaps autoCaps(fContext, fCaps); // Copy all referenced built-in functions into the Program. Transform::FindAndDeclareBuiltinFunctions(program); // Variables defined in the pre-includes need their declaring elements added to the program. Transform::FindAndDeclareBuiltinVariables(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 program usage is still correct after finalization is complete. SkASSERT(*program.usage() == *Analysis::GetUsage(program)); return this->errorCount() == 0; } #if defined(SKSL_STANDALONE) || defined(SK_GANESH) || defined(SK_GRAPHITE) #if defined(SK_ENABLE_SPIRV_VALIDATION) static bool validate_spirv(ErrorReporter& reporter, std::string_view program) { SkASSERT(0 == program.size() % 4); const uint32_t* programData = reinterpret_cast(program.data()); size_t programSize = program.size() / 4; spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_0); std::string errors; auto msgFn = [&errors](spv_message_level_t, const char*, const spv_position_t&, const char* m) { errors += "SPIR-V validation error: "; errors += m; errors += '\n'; }; tools.SetMessageConsumer(msgFn); // Verify that the SPIR-V we produced is valid. At runtime, we will abort() with a message // explaining the error. In standalone mode (skslc), we will send the message, plus the // entire disassembled SPIR-V (for easier context & debugging) as *our* error message. bool result = tools.Validate(programData, programSize); if (!result) { #if defined(SKSL_STANDALONE) // Convert the string-stream to a SPIR-V disassembly. std::string disassembly; if (tools.Disassemble(programData, programSize, &disassembly)) { errors.append(disassembly); } reporter.error(Position(), errors); #else SkDEBUGFAILF("%s", errors.c_str()); #endif } return result; } #endif bool Compiler::toSPIRV(Program& program, OutputStream& out) { TRACE_EVENT0("skia.shaders", "SkSL::Compiler::toSPIRV"); AutoSource as(this, *program.fSource); AutoShaderCaps autoCaps(fContext, fCaps); ProgramSettings settings; settings.fUseMemoryPool = false; dsl::Start(this, program.fConfig->fKind, settings); dsl::SetErrorReporter(&fErrorReporter); fSymbolTable = program.fSymbols; #ifdef SK_ENABLE_SPIRV_VALIDATION StringStream buffer; SPIRVCodeGenerator cg(fContext.get(), &program, &buffer); bool result = cg.generateCode(); if (result && program.fConfig->fSettings.fValidateSPIRV) { std::string_view binary = buffer.str(); result = validate_spirv(this->errorReporter(), binary); out.write(binary.data(), binary.size()); } #else SPIRVCodeGenerator cg(fContext.get(), &program, &out); bool result = cg.generateCode(); #endif dsl::End(); return result; } bool Compiler::toSPIRV(Program& program, std::string* out) { StringStream buffer; bool result = this->toSPIRV(program, buffer); if (result) { *out = buffer.str(); } return result; } bool Compiler::toGLSL(Program& program, OutputStream& out) { TRACE_EVENT0("skia.shaders", "SkSL::Compiler::toGLSL"); AutoSource as(this, *program.fSource); AutoShaderCaps autoCaps(fContext, fCaps); GLSLCodeGenerator cg(fContext.get(), &program, &out); bool result = cg.generateCode(); return result; } bool Compiler::toGLSL(Program& program, std::string* out) { StringStream buffer; bool result = this->toGLSL(program, buffer); if (result) { *out = buffer.str(); } return result; } bool Compiler::toHLSL(Program& program, OutputStream& out) { TRACE_EVENT0("skia.shaders", "SkSL::Compiler::toHLSL"); std::string hlsl; if (!this->toHLSL(program, &hlsl)) { return false; } out.writeString(hlsl); return true; } bool Compiler::toHLSL(Program& program, std::string* out) { std::string spirv; if (!this->toSPIRV(program, &spirv)) { return false; } if (!SPIRVtoHLSL(spirv, out)) { fErrorText += "HLSL cross-compilation not enabled"; return false; } return true; } bool Compiler::toMetal(Program& program, OutputStream& out) { TRACE_EVENT0("skia.shaders", "SkSL::Compiler::toMetal"); AutoSource as(this, *program.fSource); AutoShaderCaps autoCaps(fContext, fCaps); MetalCodeGenerator cg(fContext.get(), &program, &out); bool result = cg.generateCode(); return result; } bool Compiler::toMetal(Program& program, std::string* out) { StringStream buffer; bool result = this->toMetal(program, buffer); if (result) { *out = buffer.str(); } return result; } #if defined(SK_ENABLE_WGSL_VALIDATION) static bool validate_wgsl(ErrorReporter& reporter, const std::string& wgsl) { tint::Source::File srcFile("", wgsl); tint::Program program(tint::reader::wgsl::Parse(&srcFile)); if (program.Diagnostics().count() > 0) { tint::diag::Formatter diagFormatter; std::string diagOutput = diagFormatter.format(program.Diagnostics()); #if defined(SKSL_STANDALONE) reporter.error(Position(), diagOutput); #else SkDEBUGFAILF("%s", diagOutput.c_str()); #endif return false; } return true; } #endif // defined(SK_ENABLE_WGSL_VALIDATION) bool Compiler::toWGSL(Program& program, OutputStream& out) { TRACE_EVENT0("skia.shaders", "SkSL::Compiler::toWGSL"); AutoSource as(this, *program.fSource); #ifdef SK_ENABLE_WGSL_VALIDATION StringStream wgsl; WGSLCodeGenerator cg(fContext.get(), &program, &wgsl); bool result = cg.generateCode(); if (result) { std::string wgslString = wgsl.str(); result = validate_wgsl(this->errorReporter(), wgslString); out.writeString(wgslString); } #else WGSLCodeGenerator cg(fContext.get(), &program, &out); bool result = cg.generateCode(); #endif return result; } #endif // defined(SKSL_STANDALONE) || defined(SK_GANESH) || defined(SK_GRAPHITE) 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) + " error"; if (count > 1) { fErrorText += "s"; } fErrorText += "\n"; } } } // namespace SkSL