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