// Copyright 2020 The Tint Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include #if TINT_BUILD_SPV_READER #include "spirv-tools/libspirv.hpp" #endif // TINT_BUILD_SPV_READER #include "tint/tint.h" namespace { enum class Format { kNone = -1, kSpirv, kSpvAsm, kWgsl, kMsl, kHlsl, }; struct Options { bool show_help = false; std::string input_filename; std::string output_file = "-"; // Default to stdout bool parse_only = false; bool dump_ast = false; bool dawn_validation = false; Format format = Format::kNone; bool emit_single_entry_point = false; tint::ast::PipelineStage stage; std::string ep_name; std::vector transforms; }; const char kUsage[] = R"(Usage: tint [options] options: --format -- Output format. If not provided, will be inferred from output filename extension: .spvasm -> spvasm .spv -> spirv .wgsl -> wgsl .metal -> msl .hlsl -> hlsl If none matches, then default to SPIR-V assembly. -ep -- Output single entry point --output-file -- Output file name. Use "-" for standard output -o -- Output file name. Use "-" for standard output --transform -- Runs transforms, name list is comma separated Available transforms: bound_array_accessors emit_vertex_point_size first_index_offset --parse-only -- Stop after parsing the input --dump-ast -- Dump the generated AST to stdout --dawn-validation -- SPIRV outputs are validated with the same flags as Dawn does. Has no effect on non-SPIRV outputs. -h -- This help text)"; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-parameter" Format parse_format(const std::string& fmt) { #pragma clang diagnostic pop #if TINT_BUILD_SPV_WRITER if (fmt == "spirv") return Format::kSpirv; if (fmt == "spvasm") return Format::kSpvAsm; #endif // TINT_BUILD_SPV_WRITER #if TINT_BUILD_WGSL_WRITER if (fmt == "wgsl") return Format::kWgsl; #endif // TINT_BUILD_WGSL_WRITER #if TINT_BUILD_MSL_WRITER if (fmt == "msl") return Format::kMsl; #endif // TINT_BUILD_MSL_WRITER #if TINT_BUILD_HLSL_WRITER if (fmt == "hlsl") return Format::kHlsl; #endif // TINT_BUILD_HLSL_WRITER return Format::kNone; } /// @param input input string /// @param suffix potential suffix string /// @returns true if input ends with the given suffix. bool ends_with(const std::string& input, const std::string& suffix) { const auto input_len = input.size(); const auto suffix_len = suffix.size(); // Avoid integer overflow. return (input_len >= suffix_len) && (input_len - suffix_len == input.rfind(suffix)); } /// @param filename the filename to inspect /// @returns the inferred format for the filename suffix #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-parameter" Format infer_format(const std::string& filename) { #pragma clang diagnostic pop #if TINT_BUILD_SPV_WRITER if (ends_with(filename, ".spv")) { return Format::kSpirv; } if (ends_with(filename, ".spvasm")) { return Format::kSpvAsm; } #endif // TINT_BUILD_SPV_WRITER #if TINT_BUILD_WGSL_WRITER if (ends_with(filename, ".wgsl")) { return Format::kWgsl; } #endif // TINT_BUILD_WGSL_WRITER #if TINT_BUILD_MSL_WRITER if (ends_with(filename, ".metal")) { return Format::kMsl; } #endif // TINT_BUILD_WGSL_WRITER return Format::kNone; } tint::ast::PipelineStage convert_to_pipeline_stage(const std::string& name) { if (name == "compute") { return tint::ast::PipelineStage::kCompute; } if (name == "fragment") { return tint::ast::PipelineStage::kFragment; } if (name == "vertex") { return tint::ast::PipelineStage::kVertex; } return tint::ast::PipelineStage::kNone; } std::vector split_transform_names(std::string list) { std::vector res; std::stringstream str(list); while (str.good()) { std::string substr; getline(str, substr, ','); res.push_back(substr); } return res; } bool ParseArgs(const std::vector& args, Options* opts) { for (size_t i = 1; i < args.size(); ++i) { const std::string& arg = args[i]; if (arg == "--format") { ++i; if (i >= args.size()) { std::cerr << "Missing value for --format argument." << std::endl; return false; } opts->format = parse_format(args[i]); if (opts->format == Format::kNone) { std::cerr << "Unknown output format: " << args[i] << std::endl; return false; } } else if (arg == "-ep") { if (i + 2 >= args.size()) { std::cerr << "Missing values for -ep" << std::endl; return false; } i++; opts->stage = convert_to_pipeline_stage(args[i]); if (opts->stage == tint::ast::PipelineStage::kNone) { std::cerr << "Invalid pipeline stage: " << args[i] << std::endl; return false; } i++; opts->ep_name = args[i]; opts->emit_single_entry_point = true; } else if (arg == "-o" || arg == "--output-name") { ++i; if (i >= args.size()) { std::cerr << "Missing value for " << arg << std::endl; return false; } opts->output_file = args[i]; } else if (arg == "-h" || arg == "--help") { opts->show_help = true; } else if (arg == "--transform") { ++i; if (i >= args.size()) { std::cerr << "Missing value for " << arg << std::endl; return false; } opts->transforms = split_transform_names(args[i]); } else if (arg == "--parse-only") { opts->parse_only = true; } else if (arg == "--dump-ast") { opts->dump_ast = true; } else if (arg == "--dawn-validation") { opts->dawn_validation = true; } else if (!arg.empty()) { if (arg[0] == '-') { std::cerr << "Unrecognized option: " << arg << std::endl; return false; } if (!opts->input_filename.empty()) { std::cerr << "More than one input file specified: '" << opts->input_filename << "' and '" << arg << "'" << std::endl; return false; } opts->input_filename = arg; } } return true; } /// Copies the content from the file named `input_file` to `buffer`, /// assuming each element in the file is of type `T`. If any error occurs, /// writes error messages to the standard error stream and returns false. /// Assumes the size of a `T` object is divisible by its required alignment. /// @returns true if we successfully read the file. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-template" template bool ReadFile(const std::string& input_file, std::vector* buffer) { #pragma clang diagnostic pop if (!buffer) { std::cerr << "The buffer pointer was null" << std::endl; return false; } FILE* file = nullptr; #if defined(_MSC_VER) fopen_s(&file, input_file.c_str(), "rb"); #else file = fopen(input_file.c_str(), "rb"); #endif if (!file) { std::cerr << "Failed to open " << input_file << std::endl; return false; } fseek(file, 0, SEEK_END); uint64_t tell_file_size = static_cast(ftell(file)); if (tell_file_size <= 0) { std::cerr << "Input file of incorrect size: " << input_file << std::endl; fclose(file); return {}; } const auto file_size = static_cast(tell_file_size); if (0 != (file_size % sizeof(T))) { std::cerr << "File " << input_file << " does not contain an integral number of objects: " << file_size << " bytes in the file, require " << sizeof(T) << " bytes per object" << std::endl; fclose(file); return false; } fseek(file, 0, SEEK_SET); buffer->clear(); buffer->resize(file_size / sizeof(T)); size_t bytes_read = fread(buffer->data(), 1, file_size, file); fclose(file); if (bytes_read != file_size) { std::cerr << "Failed to read " << input_file << std::endl; return false; } return true; } /// Writes the given `buffer` into the file named as `output_file` using the /// given `mode`. If `output_file` is empty or "-", writes to standard /// output. If any error occurs, returns false and outputs error message to /// standard error. The ContainerT type must have data() and size() methods, /// like `std::string` and `std::vector` do. /// @returns true on success template bool WriteFile(const std::string& output_file, const std::string mode, const ContainerT& buffer) { const bool use_stdout = output_file.empty() || output_file == "-"; FILE* file = stdout; if (!use_stdout) { #if defined(_MSC_VER) fopen_s(&file, output_file.c_str(), mode.c_str()); #else file = fopen(output_file.c_str(), mode.c_str()); #endif if (!file) { std::cerr << "Could not open file " << output_file << " for writing" << std::endl; return false; } } size_t written = fwrite(buffer.data(), sizeof(typename ContainerT::value_type), buffer.size(), file); if (buffer.size() != written) { if (use_stdout) { std::cerr << "Could not write all output to standard output" << std::endl; } else { std::cerr << "Could not write to file " << output_file << std::endl; fclose(file); } return false; } if (!use_stdout) { fclose(file); } return true; } #if TINT_BUILD_SPV_WRITER std::string Disassemble(const std::vector& data) { std::string spv_errors; spv_target_env target_env = SPV_ENV_UNIVERSAL_1_0; auto msg_consumer = [&spv_errors](spv_message_level_t level, const char*, const spv_position_t& position, const char* message) { switch (level) { case SPV_MSG_FATAL: case SPV_MSG_INTERNAL_ERROR: case SPV_MSG_ERROR: spv_errors += "error: line " + std::to_string(position.index) + ": " + message + "\n"; break; case SPV_MSG_WARNING: spv_errors += "warning: line " + std::to_string(position.index) + ": " + message + "\n"; break; case SPV_MSG_INFO: spv_errors += "info: line " + std::to_string(position.index) + ": " + message + "\n"; break; case SPV_MSG_DEBUG: break; } }; spvtools::SpirvTools tools(target_env); tools.SetMessageConsumer(msg_consumer); std::string result; if (!tools.Disassemble(data, &result, SPV_BINARY_TO_TEXT_OPTION_INDENT | SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) { std::cerr << spv_errors << std::endl; } return result; } #endif // TINT_BUILD_SPV_WRITER } // namespace int main(int argc, const char** argv) { std::vector args(argv, argv + argc); Options options; if (!ParseArgs(args, &options)) { std::cerr << "Failed to parse arguments." << std::endl; return 1; } if (options.show_help) { std::cout << kUsage << std::endl; return 0; } // Implement output format defaults. if (options.format == Format::kNone) { // Try inferring from filename. options.format = infer_format(options.output_file); } if (options.format == Format::kNone) { // Ultimately, default to SPIR-V assembly. That's nice for interactive use. options.format = Format::kSpvAsm; } auto diag_printer = tint::diag::Printer::create(stderr, true); tint::diag::Formatter diag_formatter; std::unique_ptr reader; std::unique_ptr source_file; #if TINT_BUILD_WGSL_READER if (options.input_filename.size() > 5 && options.input_filename.substr(options.input_filename.size() - 5) == ".wgsl") { std::vector data; if (!ReadFile(options.input_filename, &data)) { return 1; } source_file = std::make_unique( options.input_filename, std::string(data.begin(), data.end())); reader = std::make_unique(source_file.get()); } #endif // TINT_BUILD_WGSL_READER #if TINT_BUILD_SPV_READER // Handle SPIR-V binary input, in files ending with .spv if (options.input_filename.size() > 4 && options.input_filename.substr(options.input_filename.size() - 4) == ".spv") { std::vector data; if (!ReadFile(options.input_filename, &data)) { return 1; } reader = std::make_unique(data); } // Handle SPIR-V assembly input, in files ending with .spvasm if (options.input_filename.size() > 7 && options.input_filename.substr(options.input_filename.size() - 7) == ".spvasm") { std::vector text; if (!ReadFile(options.input_filename, &text)) { return 1; } // Use Vulkan 1.1, since this is what Tint, internally, is expecting. spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) { std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl; }); std::vector data; if (!tools.Assemble(text.data(), text.size(), &data, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) { return 1; } reader = std::make_unique(data); } #endif // TINT_BUILD_SPV_READER if (!reader) { std::cerr << "Failed to create reader for input file: " << options.input_filename << std::endl; return 1; } if (!reader->Parse()) { diag_formatter.format(reader->diagnostics(), diag_printer.get()); return 1; } auto mod = reader->module(); if (!mod.IsValid()) { std::cerr << "Invalid module generated..." << std::endl; return 1; } tint::TypeDeterminer td(&mod); if (!td.Determine()) { std::cerr << "Type Determination: " << td.error() << std::endl; return 1; } if (options.dump_ast) { std::cout << std::endl << mod.to_str() << std::endl; } if (options.parse_only) { return 1; } tint::Validator v; if (!v.Validate(&mod)) { diag_formatter.format(v.diagnostics(), diag_printer.get()); return 1; } tint::transform::Manager transform_manager; for (const auto& name : options.transforms) { // TODO(dsinclair): The vertex pulling transform requires setup code to // be run that needs user input. Should we find a way to support that here // maybe through a provided file? if (name == "bound_array_accessors") { transform_manager.append( std::make_unique()); } else if (name == "emit_vertex_point_size") { transform_manager.append( std::make_unique()); } else if (name == "first_index_offset") { transform_manager.append( std::make_unique(0, 0)); } else { std::cerr << "Unknown transform name: " << name << std::endl; return 1; } } auto out = transform_manager.Run(&mod); if (out.diagnostics.contains_errors()) { diag_formatter.format(out.diagnostics, diag_printer.get()); return 1; } mod = std::move(out.module); std::unique_ptr writer; #if TINT_BUILD_SPV_WRITER if (options.format == Format::kSpirv || options.format == Format::kSpvAsm) { writer = std::make_unique(std::move(mod)); } #endif // TINT_BUILD_SPV_WRITER #if TINT_BUILD_WGSL_WRITER if (options.format == Format::kWgsl) { writer = std::make_unique(std::move(mod)); } #endif // TINT_BUILD_WGSL_WRITER #if TINT_BUILD_MSL_WRITER if (options.format == Format::kMsl) { writer = std::make_unique(std::move(mod)); } #endif // TINT_BUILD_MSL_WRITER #if TINT_BUILD_HLSL_WRITER if (options.format == Format::kHlsl) { writer = std::make_unique(std::move(mod)); } #endif // TINT_BUILD_HLSL_WRITER if (!writer) { std::cerr << "Unknown output format specified" << std::endl; return 1; } if (options.emit_single_entry_point) { if (!writer->GenerateEntryPoint(options.stage, options.ep_name)) { std::cerr << "Failed to generate: " << writer->error() << std::endl; return 1; } } else { if (!writer->Generate()) { std::cerr << "Failed to generate: " << writer->error() << std::endl; return 1; } } #if TINT_BUILD_SPV_WRITER bool dawn_validation_failed = false; std::ostringstream stream; if (options.dawn_validation) { // Use Vulkan 1.1, since this is what Tint, internally, uses. spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); tools.SetMessageConsumer([&stream](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) { stream << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl; }); auto* w = static_cast(writer.get()); if (!tools.Validate(w->result().data(), w->result().size(), spvtools::ValidatorOptions())) { dawn_validation_failed = true; } } if (options.format == Format::kSpvAsm) { auto* w = static_cast(writer.get()); auto str = Disassemble(w->result()); if (!WriteFile(options.output_file, "w", str)) { return 1; } } if (options.format == Format::kSpirv) { auto* w = static_cast(writer.get()); if (!WriteFile(options.output_file, "wb", w->result())) { return 1; } } if (dawn_validation_failed) { std::cerr << std::endl << std::endl << "Validation Failure:" << std::endl; std::cerr << stream.str(); return 1; } #endif // TINT_BUILD_SPV_WRITER if (options.format != Format::kSpvAsm && options.format != Format::kSpirv) { auto* w = static_cast(writer.get()); if (!WriteFile(options.output_file, "w", w->result())) { return 1; } } return 0; }