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