| /* |
| * Copyright 2022 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #define SK_OPTS_NS sksl_minify_standalone |
| #include "include/core/SkStream.h" |
| #include "include/private/SkSLProgramKind.h" |
| #include "src/base/SkStringView.h" |
| #include "src/core/SkCpu.h" |
| #include "src/core/SkOpts.h" |
| #include "src/opts/SkChecksum_opts.h" |
| #include "src/opts/SkVM_opts.h" |
| #include "src/sksl/SkSLCompiler.h" |
| #include "src/sksl/SkSLFileOutputStream.h" |
| #include "src/sksl/SkSLLexer.h" |
| #include "src/sksl/SkSLModuleLoader.h" |
| #include "src/sksl/SkSLProgramSettings.h" |
| #include "src/sksl/SkSLStringStream.h" |
| #include "src/sksl/SkSLUtil.h" |
| #include "src/sksl/transform/SkSLTransform.h" |
| #include "src/utils/SkOSPath.h" |
| #include "tools/SkGetExecutablePath.h" |
| #include "tools/skslc/ProcessWorklist.h" |
| |
| #include <cctype> |
| #include <forward_list> |
| #include <fstream> |
| #include <limits.h> |
| #include <optional> |
| #include <stdarg.h> |
| #include <stdio.h> |
| |
| static bool gUnoptimized = false; |
| static bool gStringify = false; |
| static SkSL::ProgramKind gProgramKind = SkSL::ProgramKind::kFragment; |
| |
| void SkDebugf(const char format[], ...) { |
| va_list args; |
| va_start(args, format); |
| vfprintf(stderr, format, args); |
| va_end(args); |
| } |
| |
| namespace SkOpts { |
| decltype(hash_fn) hash_fn = SK_OPTS_NS::hash_fn; |
| decltype(interpret_skvm) interpret_skvm = SK_OPTS_NS::interpret_skvm; |
| size_t raster_pipeline_highp_stride = 1; |
| } |
| |
| static std::string base_name(const std::string& path) { |
| size_t slashPos = path.find_last_of("/\\"); |
| return path.substr(slashPos == std::string::npos ? 0 : slashPos + 1); |
| } |
| |
| static std::string remove_extension(const std::string& path) { |
| size_t dotPos = path.find_last_of('.'); |
| return path.substr(0, dotPos); |
| } |
| |
| /** |
| * Displays a usage banner; used when the command line arguments don't make sense. |
| */ |
| static void show_usage() { |
| printf("usage: sksl-minify <output> <input> [--frag|--vert|--compute|--shader|" |
| "--colorfilter|--blender] [dependencies...]\n"); |
| } |
| |
| static std::string_view stringize(const SkSL::Token& token, std::string_view text) { |
| return text.substr(token.fOffset, token.fLength); |
| } |
| |
| static bool maybe_identifier(char c) { |
| return std::isalnum(c) || c == '$' || c == '_'; |
| } |
| |
| static std::forward_list<std::unique_ptr<const SkSL::Module>> compile_module_list( |
| SkSpan<const std::string> paths, SkSL::ProgramKind kind) { |
| std::forward_list<std::unique_ptr<const SkSL::Module>> modules; |
| |
| // If we are compiling a Runtime Effect... |
| if (SkSL::ProgramConfig::IsRuntimeEffect(kind)) { |
| // ... the parent modules still need to be compiled as Fragment programs. |
| // If no modules are explicitly specified, we automatically include the built-in modules for |
| // runtime effects (sksl_shared, sksl_public) so that casual users don't need to always |
| // remember to specify these modules. |
| if (paths.size() == 1) { |
| const std::string minifyDir = SkOSPath::Dirname(SkGetExecutablePath().c_str()).c_str(); |
| std::string defaultRuntimeShaderPaths[] = { |
| minifyDir + SkOSPath::SEPARATOR + "sksl_public.sksl", |
| minifyDir + SkOSPath::SEPARATOR + "sksl_shared.sksl", |
| }; |
| modules = compile_module_list(defaultRuntimeShaderPaths, SkSL::ProgramKind::kFragment); |
| } else { |
| // The parent modules were listed on the command line; we need to compile them as |
| // fragment programs. The final module keeps the Runtime Shader program-kind. |
| modules = compile_module_list(paths.subspan(1), SkSL::ProgramKind::kFragment); |
| paths = paths.first(1); |
| } |
| // Set up the public type aliases so that Runtime Shader code with GLSL types works as-is. |
| SkSL::ModuleLoader::Get().addPublicTypeAliases(modules.front().get()); |
| } |
| |
| // Load in each input as a module, from right to left. |
| // Each module inherits the symbols from its parent module. |
| SkSL::Compiler compiler(SkSL::ShaderCapsFactory::Standalone()); |
| for (auto modulePath = paths.rbegin(); modulePath != paths.rend(); ++modulePath) { |
| std::ifstream in(*modulePath); |
| std::string moduleSource{std::istreambuf_iterator<char>(in), |
| std::istreambuf_iterator<char>()}; |
| if (in.rdstate()) { |
| printf("error reading '%s'\n", modulePath->c_str()); |
| return {}; |
| } |
| |
| const SkSL::Module* parent = modules.empty() ? SkSL::ModuleLoader::Get().rootModule() |
| : modules.front().get(); |
| std::unique_ptr<SkSL::Module> m = |
| compiler.compileModule(kind, |
| modulePath->c_str(), |
| std::move(moduleSource), |
| parent, |
| SkSL::ModuleLoader::Get().coreModifiers(), |
| /*shouldInline=*/false); |
| if (!m) { |
| return {}; |
| } |
| if (!gUnoptimized) { |
| // We need to optimize every module in the chain. We rename private functions at global |
| // scope, and we need to make sure there are no name collisions between nested modules. |
| // (i.e., if module A claims names `$a` and `$b` at global scope, module B will need to |
| // start at `$c`. The most straightforward way to handle this is to actually perform the |
| // renames.) |
| compiler.optimizeModuleBeforeMinifying(kind, *m); |
| } |
| modules.push_front(std::move(m)); |
| } |
| // Return all of the modules to transfer their ownership to the caller. |
| return modules; |
| } |
| |
| static bool generate_minified_text(std::string_view inputPath, |
| std::string_view text, |
| SkSL::FileOutputStream& out) { |
| using TokenKind = SkSL::Token::Kind; |
| |
| SkSL::Lexer lexer; |
| lexer.start(text); |
| |
| SkSL::Token token; |
| std::string_view lastTokenText = " "; |
| int lineWidth = 1; |
| for (;;) { |
| token = lexer.next(); |
| if (token.fKind == TokenKind::TK_END_OF_FILE) { |
| break; |
| } |
| if (token.fKind == TokenKind::TK_LINE_COMMENT || |
| token.fKind == TokenKind::TK_BLOCK_COMMENT || |
| token.fKind == TokenKind::TK_WHITESPACE) { |
| continue; |
| } |
| std::string_view thisTokenText = stringize(token, text); |
| if (token.fKind == TokenKind::TK_INVALID) { |
| printf("%.*s: unable to parse '%.*s' at offset %d\n", |
| (int)inputPath.size(), inputPath.data(), |
| (int)thisTokenText.size(), thisTokenText.data(), |
| token.fOffset); |
| return false; |
| } |
| if (thisTokenText.empty()) { |
| continue; |
| } |
| if (token.fKind == TokenKind::TK_FLOAT_LITERAL) { |
| // We can reduce `3.0` to `3.` safely. |
| if (skstd::contains(thisTokenText, '.')) { |
| while (thisTokenText.back() == '0' && thisTokenText.size() >= 3) { |
| thisTokenText.remove_suffix(1); |
| } |
| } |
| // We can reduce `0.5` to `.5` safely. |
| if (skstd::starts_with(thisTokenText, "0.") && thisTokenText.size() >= 3) { |
| thisTokenText.remove_prefix(1); |
| } |
| } |
| SkASSERT(!lastTokenText.empty()); |
| if (gStringify && lineWidth > 75) { |
| // We're getting full-ish; wrap to a new line. |
| out.writeText("\"\n\""); |
| lineWidth = 1; |
| } |
| if (maybe_identifier(lastTokenText.back()) && maybe_identifier(thisTokenText.front())) { |
| // We are about to put two alphanumeric characters side-by-side; add whitespace between |
| // the tokens. |
| out.writeText(" "); |
| lineWidth++; |
| } |
| out.write(thisTokenText.data(), thisTokenText.size()); |
| lineWidth += thisTokenText.size(); |
| lastTokenText = thisTokenText; |
| } |
| |
| return true; |
| } |
| |
| static bool find_boolean_flag(SkSpan<std::string>* args, std::string_view flagName) { |
| size_t startingCount = args->size(); |
| auto iter = std::remove_if(args->begin(), args->end(), |
| [&](const std::string& a) { return a == flagName; }); |
| *args = args->subspan(0, std::distance(args->begin(), iter)); |
| return args->size() < startingCount; |
| } |
| |
| static bool has_overlapping_flags(SkSpan<const bool> flags) { |
| // Returns true if more than one boolean is set. |
| return std::count(flags.begin(), flags.end(), true) > 1; |
| } |
| |
| static ResultCode process_command(SkSpan<std::string> args) { |
| // Ignore the process name. |
| SkASSERT(!args.empty()); |
| args = args.subspan(1); |
| |
| // Process command line flags. |
| gUnoptimized = find_boolean_flag(&args, "--unoptimized"); |
| gStringify = find_boolean_flag(&args, "--stringify"); |
| bool isFrag = find_boolean_flag(&args, "--frag"); |
| bool isVert = find_boolean_flag(&args, "--vert"); |
| bool isCompute = find_boolean_flag(&args, "--compute"); |
| bool isShader = find_boolean_flag(&args, "--shader"); |
| bool isColorFilter = find_boolean_flag(&args, "--colorfilter"); |
| bool isBlender = find_boolean_flag(&args, "--blender"); |
| if (has_overlapping_flags({isFrag, isVert, isCompute, isShader, isColorFilter, isBlender})) { |
| show_usage(); |
| return ResultCode::kInputError; |
| } |
| if (isFrag) { |
| gProgramKind = SkSL::ProgramKind::kFragment; |
| } else if (isVert) { |
| gProgramKind = SkSL::ProgramKind::kVertex; |
| } else if (isCompute) { |
| gProgramKind = SkSL::ProgramKind::kCompute; |
| } else if (isColorFilter) { |
| gProgramKind = SkSL::ProgramKind::kRuntimeColorFilter; |
| } else if (isBlender) { |
| gProgramKind = SkSL::ProgramKind::kRuntimeBlender; |
| } else { |
| // Default case, if no option is specified. |
| gProgramKind = SkSL::ProgramKind::kRuntimeShader; |
| } |
| |
| // We expect, at a minimum, an output path and one or more input paths. |
| if (args.size() < 2) { |
| show_usage(); |
| return ResultCode::kInputError; |
| } |
| const std::string& outputPath = args[0]; |
| SkSpan inputPaths = args.subspan(1); |
| |
| // Compile the original SkSL from the input path. |
| std::forward_list<std::unique_ptr<const SkSL::Module>> modules = |
| compile_module_list(inputPaths, gProgramKind); |
| if (modules.empty()) { |
| return ResultCode::kInputError; |
| } |
| const SkSL::Module* module = modules.front().get(); |
| |
| // Emit the minified SkSL into our output path. |
| SkSL::FileOutputStream out(outputPath.c_str()); |
| if (!out.isValid()) { |
| printf("error writing '%s'\n", outputPath.c_str()); |
| return ResultCode::kOutputError; |
| } |
| |
| std::string baseName = remove_extension(base_name(inputPaths.front())); |
| if (gStringify) { |
| out.printf("static constexpr char SKSL_MINIFIED_%s[] =\n\"", baseName.c_str()); |
| } |
| |
| // Generate the program text by getting the program's description. |
| std::string text; |
| for (const std::unique_ptr<SkSL::ProgramElement>& element : module->fElements) { |
| text += element->description(); |
| } |
| |
| // Eliminate whitespace and perform other basic simplifications via a lexer pass. |
| if (!generate_minified_text(inputPaths.front(), text, out)) { |
| return ResultCode::kInputError; |
| } |
| |
| if (gStringify) { |
| out.writeText("\";"); |
| } |
| out.writeText("\n"); |
| |
| if (!out.close()) { |
| printf("error writing '%s'\n", outputPath.c_str()); |
| return ResultCode::kOutputError; |
| } |
| |
| return ResultCode::kSuccess; |
| } |
| |
| int main(int argc, const char** argv) { |
| if (argc == 2) { |
| // Worklists are the only two-argument case for sksl-minify, and we don't intend to support |
| // nested worklists, so we can process them here. |
| return (int)ProcessWorklist(argv[1], process_command); |
| } else { |
| // Process non-worklist inputs. |
| std::vector<std::string> args; |
| for (int index=0; index<argc; ++index) { |
| args.push_back(argv[index]); |
| } |
| |
| return (int)process_command(args); |
| } |
| } |