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