• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "src/base/SkStringView.h"
9 #include "src/core/SkOpts.h"
10 #include "src/sksl/SkSLCompiler.h"
11 #include "src/sksl/SkSLFileOutputStream.h"
12 #include "src/sksl/SkSLLexer.h"
13 #include "src/sksl/SkSLModuleLoader.h"
14 #include "src/sksl/SkSLProgramKind.h"
15 #include "src/sksl/SkSLProgramSettings.h"
16 #include "src/sksl/SkSLUtil.h"
17 #include "src/sksl/ir/SkSLStructDefinition.h"
18 #include "src/sksl/ir/SkSLSymbolTable.h"
19 #include "src/sksl/transform/SkSLTransform.h"
20 #include "src/utils/SkOSPath.h"
21 #include "tools/SkGetExecutablePath.h"
22 #include "tools/skslc/ProcessWorklist.h"
23 
24 #include <cctype>
25 #include <forward_list>
26 #include <fstream>
27 #include <limits.h>
28 #include <stdarg.h>
29 #include <stdio.h>
30 
31 static bool gUnoptimized = false;
32 static bool gStringify = false;
33 static SkSL::ProgramKind gProgramKind = SkSL::ProgramKind::kFragment;
34 
SkDebugf(const char format[],...)35 void SkDebugf(const char format[], ...) {
36     va_list args;
37     va_start(args, format);
38     vfprintf(stderr, format, args);
39     va_end(args);
40 }
41 
42 namespace SkOpts {
43     size_t raster_pipeline_highp_stride = 1;
44 }
45 
base_name(const std::string & path)46 static std::string base_name(const std::string& path) {
47     size_t slashPos = path.find_last_of("/\\");
48     return path.substr(slashPos == std::string::npos ? 0 : slashPos + 1);
49 }
50 
remove_extension(const std::string & path)51 static std::string remove_extension(const std::string& path) {
52     size_t dotPos = path.find_last_of('.');
53     return path.substr(0, dotPos);
54 }
55 
56 /**
57  * Displays a usage banner; used when the command line arguments don't make sense.
58  */
show_usage()59 static void show_usage() {
60     printf("usage: sksl-minify <output> <input> [--frag|--vert|--compute|--shader|"
61            "--colorfilter|--blender|--meshfrag|--meshvert] [dependencies...]\n");
62 }
63 
stringize(const SkSL::Token & token,std::string_view text)64 static std::string_view stringize(const SkSL::Token& token, std::string_view text) {
65     return text.substr(token.fOffset, token.fLength);
66 }
67 
maybe_identifier(char c)68 static bool maybe_identifier(char c) {
69     return std::isalnum(c) || c == '$' || c == '_';
70 }
71 
is_plus_or_minus(char c)72 static bool is_plus_or_minus(char c) {
73     return c == '+' || c == '-';
74 }
75 
compile_module_list(SkSpan<const std::string> paths,SkSL::ProgramKind kind)76 static std::forward_list<std::unique_ptr<const SkSL::Module>> compile_module_list(
77         SkSpan<const std::string> paths, SkSL::ProgramKind kind) {
78     std::forward_list<std::unique_ptr<const SkSL::Module>> modules;
79 
80     // If we are compiling a Runtime Effect...
81     if (SkSL::ProgramConfig::IsRuntimeEffect(kind)) {
82         // ... the parent modules still need to be compiled as Fragment programs.
83         // If no modules are explicitly specified, we automatically include the built-in modules for
84         // runtime effects (sksl_shared, sksl_public) so that casual users don't need to always
85         // remember to specify these modules.
86         if (paths.size() == 1) {
87             const std::string minifyDir = SkOSPath::Dirname(SkGetExecutablePath().c_str()).c_str();
88             std::string defaultRuntimeShaderPaths[] = {
89                     minifyDir + SkOSPath::SEPARATOR + "sksl_public.sksl",
90                     minifyDir + SkOSPath::SEPARATOR + "sksl_shared.sksl",
91             };
92             modules = compile_module_list(defaultRuntimeShaderPaths, SkSL::ProgramKind::kFragment);
93         } else {
94             // The parent modules were listed on the command line; we need to compile them as
95             // fragment programs. The final module keeps the Runtime Shader program-kind.
96             modules = compile_module_list(paths.subspan(1), SkSL::ProgramKind::kFragment);
97             paths = paths.first(1);
98         }
99         // Set up the public type aliases so that Runtime Shader code with GLSL types works as-is.
100         SkSL::ModuleLoader::Get().addPublicTypeAliases(modules.front().get());
101     }
102 
103     // Load in each input as a module, from right to left.
104     // Each module inherits the symbols from its parent module.
105     SkSL::Compiler compiler;
106     for (auto modulePath = paths.rbegin(); modulePath != paths.rend(); ++modulePath) {
107         std::ifstream in(*modulePath);
108         std::string moduleSource{std::istreambuf_iterator<char>(in),
109                                  std::istreambuf_iterator<char>()};
110         if (in.rdstate()) {
111             printf("error reading '%s'\n", modulePath->c_str());
112             return {};
113         }
114 
115         const SkSL::Module* parent = modules.empty() ? SkSL::ModuleLoader::Get().rootModule()
116                                                      : modules.front().get();
117         std::unique_ptr<SkSL::Module> m = compiler.compileModule(kind,
118                                                                  modulePath->c_str(),
119                                                                  std::move(moduleSource),
120                                                                  parent,
121                                                                  /*shouldInline=*/false);
122         if (!m) {
123             return {};
124         }
125         // We need to optimize every module in the chain. We rename private functions at global
126         // scope, and we need to make sure there are no name collisions between nested modules.
127         // (i.e., if module A claims names `$a` and `$b` at global scope, module B will need to
128         // start at `$c`. The most straightforward way to handle this is to actually perform the
129         // renames.)
130         compiler.optimizeModuleBeforeMinifying(kind, *m, /*shrinkSymbols=*/!gUnoptimized);
131         modules.push_front(std::move(m));
132     }
133     // Return all of the modules to transfer their ownership to the caller.
134     return modules;
135 }
136 
generate_minified_text(std::string_view inputPath,std::string_view text,SkSL::FileOutputStream & out)137 static bool generate_minified_text(std::string_view inputPath,
138                                    std::string_view text,
139                                    SkSL::FileOutputStream& out) {
140     using TokenKind = SkSL::Token::Kind;
141 
142     SkSL::Lexer lexer;
143     lexer.start(text);
144 
145     SkSL::Token token;
146     std::string_view lastTokenText = " ";
147     int lineWidth = 1;
148     for (;;) {
149         token = lexer.next();
150         if (token.fKind == TokenKind::TK_END_OF_FILE) {
151             break;
152         }
153         if (token.fKind == TokenKind::TK_LINE_COMMENT ||
154             token.fKind == TokenKind::TK_BLOCK_COMMENT ||
155             token.fKind == TokenKind::TK_WHITESPACE) {
156             continue;
157         }
158         std::string_view thisTokenText = stringize(token, text);
159         if (token.fKind == TokenKind::TK_INVALID) {
160             printf("%.*s: unable to parse '%.*s' at offset %d\n",
161                    (int)inputPath.size(), inputPath.data(),
162                    (int)thisTokenText.size(), thisTokenText.data(),
163                    token.fOffset);
164             return false;
165         }
166         if (thisTokenText.empty()) {
167             continue;
168         }
169         if (token.fKind == TokenKind::TK_FLOAT_LITERAL) {
170             // We can reduce `3.0` to `3.` safely.
171             if (skstd::contains(thisTokenText, '.')) {
172                 while (thisTokenText.back() == '0' && thisTokenText.size() >= 3) {
173                     thisTokenText.remove_suffix(1);
174                 }
175             }
176             // We can reduce `0.5` to `.5` safely.
177             if (skstd::starts_with(thisTokenText, "0.") && thisTokenText.size() >= 3) {
178                 thisTokenText.remove_prefix(1);
179             }
180         }
181         SkASSERT(!lastTokenText.empty());
182         if (gStringify && lineWidth > 75) {
183             // We're getting full-ish; wrap to a new line.
184             out.writeText("\"\n\"");
185             lineWidth = 1;
186         }
187 
188         // Detect tokens with abutting alphanumeric characters side-by-side.
189         bool adjacentIdentifiers =
190                 maybe_identifier(lastTokenText.back()) && maybe_identifier(thisTokenText.front());
191 
192         // Detect potentially ambiguous preincrement/postincrement operators.
193         // For instance, `x + ++y` and `x++ + y` require whitespace for differentiation.
194         bool adjacentPlusOrMinus =
195                 is_plus_or_minus(lastTokenText.back()) && is_plus_or_minus(thisTokenText.front());
196 
197         // Insert whitespace when it is necessary for program correctness.
198         if (adjacentIdentifiers || adjacentPlusOrMinus) {
199             out.writeText(" ");
200             lineWidth++;
201         }
202         out.write(thisTokenText.data(), thisTokenText.size());
203         lineWidth += thisTokenText.size();
204         lastTokenText = thisTokenText;
205     }
206 
207     return true;
208 }
209 
find_boolean_flag(SkSpan<std::string> * args,std::string_view flagName)210 static bool find_boolean_flag(SkSpan<std::string>* args, std::string_view flagName) {
211     size_t startingCount = args->size();
212     auto iter = std::remove_if(args->begin(), args->end(),
213                                [&](const std::string& a) { return a == flagName; });
214     *args = args->subspan(0, std::distance(args->begin(), iter));
215     return args->size() < startingCount;
216 }
217 
has_overlapping_flags(SkSpan<const bool> flags)218 static bool has_overlapping_flags(SkSpan<const bool> flags) {
219     // Returns true if more than one boolean is set.
220     return std::count(flags.begin(), flags.end(), true) > 1;
221 }
222 
process_command(SkSpan<std::string> args)223 static ResultCode process_command(SkSpan<std::string> args) {
224     // Ignore the process name.
225     SkASSERT(!args.empty());
226     args = args.subspan(1);
227 
228     // Process command line flags.
229     gUnoptimized = find_boolean_flag(&args, "--unoptimized");
230     gStringify = find_boolean_flag(&args, "--stringify");
231     bool isFrag = find_boolean_flag(&args, "--frag");
232     bool isVert = find_boolean_flag(&args, "--vert");
233     bool isCompute = find_boolean_flag(&args, "--compute");
234     bool isShader = find_boolean_flag(&args, "--shader");
235     bool isPrivateShader = find_boolean_flag(&args, "--privshader");
236     bool isColorFilter = find_boolean_flag(&args, "--colorfilter");
237     bool isBlender = find_boolean_flag(&args, "--blender");
238     bool isMeshFrag = find_boolean_flag(&args, "--meshfrag");
239     bool isMeshVert = find_boolean_flag(&args, "--meshvert");
240     if (has_overlapping_flags({isFrag, isVert, isCompute, isShader, isColorFilter,
241                                isBlender, isMeshFrag, isMeshVert})) {
242         show_usage();
243         return ResultCode::kInputError;
244     }
245     if (isFrag) {
246         gProgramKind = SkSL::ProgramKind::kFragment;
247     } else if (isVert) {
248         gProgramKind = SkSL::ProgramKind::kVertex;
249     } else if (isCompute) {
250         gProgramKind = SkSL::ProgramKind::kCompute;
251     } else if (isColorFilter) {
252         gProgramKind = SkSL::ProgramKind::kRuntimeColorFilter;
253     } else if (isBlender) {
254         gProgramKind = SkSL::ProgramKind::kRuntimeBlender;
255     } else if (isMeshFrag) {
256         gProgramKind = SkSL::ProgramKind::kMeshFragment;
257     } else if (isMeshVert) {
258         gProgramKind = SkSL::ProgramKind::kMeshVertex;
259     } else if (isPrivateShader) {
260         gProgramKind = SkSL::ProgramKind::kPrivateRuntimeShader;
261     } else {
262         // Default case, if no option is specified.
263         gProgramKind = SkSL::ProgramKind::kRuntimeShader;
264     }
265 
266     // We expect, at a minimum, an output path and one or more input paths.
267     if (args.size() < 2) {
268         show_usage();
269         return ResultCode::kInputError;
270     }
271     const std::string& outputPath = args[0];
272     SkSpan inputPaths = args.subspan(1);
273 
274     // Compile the original SkSL from the input path.
275     std::forward_list<std::unique_ptr<const SkSL::Module>> modules =
276             compile_module_list(inputPaths, gProgramKind);
277     if (modules.empty()) {
278         return ResultCode::kInputError;
279     }
280     const SkSL::Module* module = modules.front().get();
281 
282     // Emit the minified SkSL into our output path.
283     SkSL::FileOutputStream out(outputPath.c_str());
284     if (!out.isValid()) {
285         printf("error writing '%s'\n", outputPath.c_str());
286         return ResultCode::kOutputError;
287     }
288 
289     std::string baseName = remove_extension(base_name(inputPaths.front()));
290     if (gStringify) {
291         out.printf("static constexpr char SKSL_MINIFIED_%s[] =\n\"", baseName.c_str());
292     }
293 
294     // Generate the program text by getting the program's description.
295     std::string text;
296     for (const std::unique_ptr<SkSL::ProgramElement>& element : module->fElements) {
297         if ((isMeshFrag || isMeshVert) && element->is<SkSL::StructDefinition>()) {
298             std::string_view name = element->as<SkSL::StructDefinition>().type().name();
299             if (name == "Attributes" || name == "Varyings") {
300                 // Don't emit the Attributes or Varyings structs from a mesh program into the
301                 // minified output; those are synthesized via the SkMeshSpecification.
302                 continue;
303             }
304         }
305         text += element->description();
306     }
307 
308     // Eliminate whitespace and perform other basic simplifications via a lexer pass.
309     if (!generate_minified_text(inputPaths.front(), text, out)) {
310         return ResultCode::kInputError;
311     }
312 
313     if (gStringify) {
314         out.writeText("\";");
315     }
316     out.writeText("\n");
317 
318     if (!out.close()) {
319         printf("error writing '%s'\n", outputPath.c_str());
320         return ResultCode::kOutputError;
321     }
322 
323     return ResultCode::kSuccess;
324 }
325 
main(int argc,const char ** argv)326 int main(int argc, const char** argv) {
327     if (argc == 2) {
328         // Worklists are the only two-argument case for sksl-minify, and we don't intend to support
329         // nested worklists, so we can process them here.
330         return (int)ProcessWorklist(argv[1], process_command);
331     } else {
332         // Process non-worklist inputs.
333         std::vector<std::string> args;
334         for (int index=0; index<argc; ++index) {
335             args.push_back(argv[index]);
336         }
337 
338         return (int)process_command(args);
339     }
340 }
341