321 lines
12 KiB
C++
321 lines
12 KiB
C++
|
|
/*
|
||
|
|
* 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);
|
||
|
|
}
|
||
|
|
}
|