1 // Copyright (c) 2018 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include <cassert>
16 #include <cerrno>
17 #include <cstdlib>
18 #include <cstring>
19 #include <functional>
20 #include <sstream>
21 
22 #include "source/opt/build_module.h"
23 #include "source/opt/ir_context.h"
24 #include "source/opt/log.h"
25 #include "source/reduce/reducer.h"
26 #include "source/spirv_reducer_options.h"
27 #include "source/util/string_utils.h"
28 #include "tools/io.h"
29 #include "tools/util/cli_consumer.h"
30 
31 namespace {
32 
33 // Execute a command using the shell.
34 // Returns true if and only if the command's exit status was 0.
ExecuteCommand(const std::string & command)35 bool ExecuteCommand(const std::string& command) {
36   errno = 0;
37   int status = std::system(command.c_str());
38   assert(errno == 0 && "failed to execute command");
39   // The result returned by 'system' is implementation-defined, but is
40   // usually the case that the returned value is 0 when the command's exit
41   // code was 0.  We are assuming that here, and that's all we depend on.
42   return status == 0;
43 }
44 
45 // Status and actions to perform after parsing command-line arguments.
46 enum ReduceActions { REDUCE_CONTINUE, REDUCE_STOP };
47 
48 struct ReduceStatus {
49   ReduceActions action;
50   int code;
51 };
52 
PrintUsage(const char * program)53 void PrintUsage(const char* program) {
54   // NOTE: Please maintain flags in lexicographical order.
55   printf(
56       R"(%s - Reduce a SPIR-V binary file with respect to a user-provided
57 interestingness test.
58 
59 USAGE: %s [options] <input.spv> -o <output.spv> -- <interestingness_test> [args...]
60 
61 The SPIR-V binary is read from <input.spv>. The reduced SPIR-V binary is
62 written to <output.spv>.
63 
64 Whether a binary is interesting is determined by <interestingness_test>, which
65 should be the path to a script. The "--" characters are optional but denote
66 that all arguments that follow are positional arguments and thus will be
67 forwarded to the interestingness test, and not parsed by %s.
68 
69  * The script must be executable.
70 
71  * The script should take the path to a SPIR-V binary file (.spv) as an
72    argument, and exit with code 0 if and only if the binary file is
73    interesting.  The binary will be passed to the script as an argument after
74    any other provided arguments [args...].
75 
76  * Example: an interestingness test for reducing a SPIR-V binary file that
77    causes tool "foo" to exit with error code 1 and print "Fatal error: bar" to
78    standard error should:
79      - invoke "foo" on the binary passed as the script argument;
80      - capture the return code and standard error from "bar";
81      - exit with code 0 if and only if the return code of "foo" was 1 and the
82        standard error from "bar" contained "Fatal error: bar".
83 
84  * The reducer does not place a time limit on how long the interestingness test
85    takes to run, so it is advisable to use per-command timeouts inside the
86    script when invoking SPIR-V-processing tools (such as "foo" in the above
87    example).
88 
89 NOTE: The reducer is a work in progress.
90 
91 Options (in lexicographical order):
92 
93   --fail-on-validation-error
94                Stop reduction with an error if any reduction step produces a
95                SPIR-V module that fails to validate.
96   -h, --help
97                Print this help.
98   --step-limit=
99                32-bit unsigned integer specifying maximum number of steps the
100                reducer will take before giving up.
101   --target-function=
102                32-bit unsigned integer specifying the id of a function in the
103                input module.  The reducer will restrict attention to this
104                function, and will not make changes to other functions or to
105                instructions outside of functions, except that some global
106                instructions may be added in support of reducing the target
107                function.  If 0 is specified (the default) then all functions are
108                reduced.
109   --temp-file-prefix=
110                Specifies a temporary file prefix that will be used to output
111                temporary shader files during reduction.  A number and .spv
112                extension will be added.  The default is "temp_", which will
113                cause files like "temp_0001.spv" to be output to the current
114                directory.
115   --version
116                Display reducer version information.
117 
118 Supported validator options are as follows. See `spirv-val --help` for details.
119   --before-hlsl-legalization
120   --relax-block-layout
121   --relax-logical-pointer
122   --relax-struct-store
123   --scalar-block-layout
124   --skip-block-layout
125 )",
126       program, program, program);
127 }
128 
129 // Message consumer for this tool.  Used to emit diagnostics during
130 // initialization and setup. Note that |source| and |position| are irrelevant
131 // here because we are still not processing a SPIR-V input file.
ReduceDiagnostic(spv_message_level_t level,const char *,const spv_position_t &,const char * message)132 void ReduceDiagnostic(spv_message_level_t level, const char* /*source*/,
133                       const spv_position_t& /*position*/, const char* message) {
134   if (level == SPV_MSG_ERROR) {
135     fprintf(stderr, "error: ");
136   }
137   fprintf(stderr, "%s\n", message);
138 }
139 
ParseFlags(int argc,const char ** argv,std::string * in_binary_file,std::string * out_binary_file,std::vector<std::string> * interestingness_test,std::string * temp_file_prefix,spvtools::ReducerOptions * reducer_options,spvtools::ValidatorOptions * validator_options)140 ReduceStatus ParseFlags(int argc, const char** argv,
141                         std::string* in_binary_file,
142                         std::string* out_binary_file,
143                         std::vector<std::string>* interestingness_test,
144                         std::string* temp_file_prefix,
145                         spvtools::ReducerOptions* reducer_options,
146                         spvtools::ValidatorOptions* validator_options) {
147   uint32_t positional_arg_index = 0;
148   bool only_positional_arguments_remain = false;
149 
150   for (int argi = 1; argi < argc; ++argi) {
151     const char* cur_arg = argv[argi];
152     if ('-' == cur_arg[0] && !only_positional_arguments_remain) {
153       if (0 == strcmp(cur_arg, "--version")) {
154         spvtools::Logf(ReduceDiagnostic, SPV_MSG_INFO, nullptr, {}, "%s\n",
155                        spvSoftwareVersionDetailsString());
156         return {REDUCE_STOP, 0};
157       } else if (0 == strcmp(cur_arg, "--help") || 0 == strcmp(cur_arg, "-h")) {
158         PrintUsage(argv[0]);
159         return {REDUCE_STOP, 0};
160       } else if (0 == strcmp(cur_arg, "-o")) {
161         if (out_binary_file->empty() && argi + 1 < argc) {
162           *out_binary_file = std::string(argv[++argi]);
163         } else {
164           PrintUsage(argv[0]);
165           return {REDUCE_STOP, 1};
166         }
167       } else if (0 == strncmp(cur_arg,
168                               "--step-limit=", sizeof("--step-limit=") - 1)) {
169         const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
170         char* end = nullptr;
171         errno = 0;
172         const auto step_limit =
173             static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
174         assert(end != split_flag.second.c_str() && errno == 0);
175         reducer_options->set_step_limit(step_limit);
176       } else if (0 == strncmp(cur_arg, "--target-function=",
177                               sizeof("--target-function=") - 1)) {
178         const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
179         char* end = nullptr;
180         errno = 0;
181         const auto target_function =
182             static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
183         assert(end != split_flag.second.c_str() && errno == 0);
184         reducer_options->set_target_function(target_function);
185       } else if (0 == strcmp(cur_arg, "--fail-on-validation-error")) {
186         reducer_options->set_fail_on_validation_error(true);
187       } else if (0 == strcmp(cur_arg, "--before-hlsl-legalization")) {
188         validator_options->SetBeforeHlslLegalization(true);
189       } else if (0 == strcmp(cur_arg, "--relax-logical-pointer")) {
190         validator_options->SetRelaxLogicalPointer(true);
191       } else if (0 == strcmp(cur_arg, "--relax-block-layout")) {
192         validator_options->SetRelaxBlockLayout(true);
193       } else if (0 == strcmp(cur_arg, "--scalar-block-layout")) {
194         validator_options->SetScalarBlockLayout(true);
195       } else if (0 == strcmp(cur_arg, "--skip-block-layout")) {
196         validator_options->SetSkipBlockLayout(true);
197       } else if (0 == strcmp(cur_arg, "--relax-struct-store")) {
198         validator_options->SetRelaxStructStore(true);
199       } else if (0 == strncmp(cur_arg, "--temp-file-prefix=",
200                               sizeof("--temp-file-prefix=") - 1)) {
201         const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
202         *temp_file_prefix = std::string(split_flag.second);
203       } else if (0 == strcmp(cur_arg, "--")) {
204         only_positional_arguments_remain = true;
205       } else {
206         std::stringstream ss;
207         ss << "Unrecognized argument: " << cur_arg << std::endl;
208         spvtools::Error(ReduceDiagnostic, nullptr, {}, ss.str().c_str());
209         PrintUsage(argv[0]);
210         return {REDUCE_STOP, 1};
211       }
212     } else if (positional_arg_index == 0) {
213       // Binary input file name
214       assert(in_binary_file->empty());
215       *in_binary_file = std::string(cur_arg);
216       positional_arg_index++;
217     } else {
218       interestingness_test->push_back(std::string(cur_arg));
219     }
220   }
221 
222   if (in_binary_file->empty()) {
223     spvtools::Error(ReduceDiagnostic, nullptr, {}, "No input file specified");
224     return {REDUCE_STOP, 1};
225   }
226 
227   if (out_binary_file->empty()) {
228     spvtools::Error(ReduceDiagnostic, nullptr, {}, "-o required");
229     return {REDUCE_STOP, 1};
230   }
231 
232   if (interestingness_test->empty()) {
233     spvtools::Error(ReduceDiagnostic, nullptr, {},
234                     "No interestingness test specified");
235     return {REDUCE_STOP, 1};
236   }
237 
238   return {REDUCE_CONTINUE, 0};
239 }
240 
241 }  // namespace
242 
243 // Dumps |binary| to file |filename|. Useful for interactive debugging.
DumpShader(const std::vector<uint32_t> & binary,const char * filename)244 void DumpShader(const std::vector<uint32_t>& binary, const char* filename) {
245   auto write_file_succeeded =
246       WriteFile(filename, "wb", &binary[0], binary.size());
247   if (!write_file_succeeded) {
248     std::cerr << "Failed to dump shader" << std::endl;
249   }
250 }
251 
252 // Dumps the SPIRV-V module in |context| to file |filename|. Useful for
253 // interactive debugging.
DumpShader(spvtools::opt::IRContext * context,const char * filename)254 void DumpShader(spvtools::opt::IRContext* context, const char* filename) {
255   std::vector<uint32_t> binary;
256   context->module()->ToBinary(&binary, false);
257   DumpShader(binary, filename);
258 }
259 
260 const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_6;
261 
main(int argc,const char ** argv)262 int main(int argc, const char** argv) {
263   std::string in_binary_file;
264   std::string out_binary_file;
265   std::vector<std::string> interestingness_test;
266   std::string temp_file_prefix = "temp_";
267 
268   spv_target_env target_env = kDefaultEnvironment;
269   spvtools::ReducerOptions reducer_options;
270   spvtools::ValidatorOptions validator_options;
271 
272   ReduceStatus status = ParseFlags(
273       argc, argv, &in_binary_file, &out_binary_file, &interestingness_test,
274       &temp_file_prefix, &reducer_options, &validator_options);
275 
276   if (status.action == REDUCE_STOP) {
277     return status.code;
278   }
279 
280   spvtools::reduce::Reducer reducer(target_env);
281 
282   std::stringstream joined;
283   joined << interestingness_test[0];
284   for (size_t i = 1, size = interestingness_test.size(); i < size; ++i) {
285     joined << " " << interestingness_test[i];
286   }
287   std::string interestingness_command_joined = joined.str();
288 
289   reducer.SetInterestingnessFunction(
290       [interestingness_command_joined, temp_file_prefix](
291           std::vector<uint32_t> binary, uint32_t reductions_applied) -> bool {
292         std::stringstream ss;
293         ss << temp_file_prefix << std::setw(4) << std::setfill('0')
294            << reductions_applied << ".spv";
295         const auto spv_file = ss.str();
296         const std::string command =
297             interestingness_command_joined + " " + spv_file;
298         auto write_file_succeeded =
299             WriteFile(spv_file.c_str(), "wb", &binary[0], binary.size());
300         (void)(write_file_succeeded);
301         assert(write_file_succeeded);
302         return ExecuteCommand(command);
303       });
304 
305   reducer.AddDefaultReductionPasses();
306 
307   reducer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
308 
309   std::vector<uint32_t> binary_in;
310   if (!ReadBinaryFile<uint32_t>(in_binary_file.c_str(), &binary_in)) {
311     return 1;
312   }
313 
314   const uint32_t target_function = (*reducer_options).target_function;
315   if (target_function) {
316     // A target function was specified; check that it exists.
317     std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
318         kDefaultEnvironment, spvtools::utils::CLIMessageConsumer,
319         binary_in.data(), binary_in.size());
320     bool found_target_function = false;
321     for (auto& function : *context->module()) {
322       if (function.result_id() == target_function) {
323         found_target_function = true;
324         break;
325       }
326     }
327     if (!found_target_function) {
328       std::stringstream strstr;
329       strstr << "Target function with id " << target_function
330              << " was requested, but not found in the module; stopping.";
331       spvtools::utils::CLIMessageConsumer(SPV_MSG_ERROR, nullptr, {},
332                                           strstr.str().c_str());
333       return 1;
334     }
335   }
336 
337   std::vector<uint32_t> binary_out;
338   const auto reduction_status = reducer.Run(std::move(binary_in), &binary_out,
339                                             reducer_options, validator_options);
340 
341   // Always try to write the output file, even if the reduction failed.
342   if (!WriteFile<uint32_t>(out_binary_file.c_str(), "wb", binary_out.data(),
343                            binary_out.size())) {
344     return 1;
345   }
346 
347   // These are the only successful statuses.
348   switch (reduction_status) {
349     case spvtools::reduce::Reducer::ReductionResultStatus::kComplete:
350     case spvtools::reduce::Reducer::ReductionResultStatus::kReachedStepLimit:
351       return 0;
352     default:
353       break;
354   }
355 
356   return 1;
357 }
358