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