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