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