| /* | |
| * Copyright 2024 Philip Rebohle for Valve Software | |
| * | |
| * This library is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Lesser General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2.1 of the License, or (at your option) any later version. | |
| * | |
| * This library is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| * Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public | |
| * License along with this library; if not, write to the Free Software | |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA | |
| */ | |
| #include <array> | |
| #include <atomic> | |
| #include <condition_variable> | |
| #include <cstring> | |
| #include <filesystem> | |
| #include <fstream> | |
| #include <iomanip> | |
| #include <iostream> | |
| #include <optional> | |
| #include <queue> | |
| #include <mutex> | |
| #include <string> | |
| #include <sstream> | |
| #include <thread> | |
| #include <utility> | |
| #include "dxbc_library.h" | |
| #include "dxil_library.h" | |
| template<typename T> | |
| std::string from_wide_string(const T &str) | |
| { | |
| std::string result; | |
| result.reserve(str.size()); | |
| for (size_t i = 0; i < str.size(); i++) | |
| result += char(str[i]); | |
| return result; | |
| } | |
| bool create_output_directory(const std::filesystem::path &path) | |
| { | |
| return std::filesystem::is_directory(path) || | |
| std::filesystem::create_directory(path); | |
| } | |
| /* Structure that stores compiler instances */ | |
| struct compiler_instances | |
| { | |
| dxc_library dxc; | |
| d3dcompiler_library d3dcompiler; | |
| }; | |
| struct shader_profile | |
| { | |
| /* Full target name for DXC */ | |
| std::wstring wide_name; | |
| /* Full target name for d3dcompiler */ | |
| std::string name; | |
| /* Compiler arguments. For DXIL, these are passed directly to | |
| * dxcapi, for d3dcompiler these match fxc arguments. */ | |
| std::vector<std::wstring> arguments; | |
| /* Whether or not the shader targets Shader Model 6 or above */ | |
| bool is_dxil = false; | |
| template<typename string_type> | |
| static std::optional<shader_profile> parse(const string_type &str) | |
| { | |
| using char_type = typename string_type::value_type; | |
| shader_profile result; | |
| uint32_t idx = 0; | |
| /* Validate profile name until delimiter. Only ASCII | |
| * characters are allowed. */ | |
| while (str[idx] && str[idx] != char_type('_')) | |
| { | |
| if (str[idx++] > char_type('\x7f')) | |
| return std::nullopt; | |
| } | |
| if (str[idx++] != char_type('_')) | |
| return std::nullopt; | |
| /* Valicate major version and check whether the profile is | |
| * DXIL (6_x) or DXBC (5_x) */ | |
| if (str[idx] < char_type('0') || str[idx] > char_type('9')) | |
| return std::nullopt; | |
| uint8_t hi_ver = uint8_t(str[idx++] - char_type('0')); | |
| if (str[idx++] != char_type('_')) | |
| return std::nullopt; | |
| result.is_dxil = hi_ver >= 6; | |
| /* Validate minor version number */ | |
| while (str[idx]) | |
| { | |
| if (str[idx] < char_type('0') || str[idx] > char_type('9')) | |
| return std::nullopt; | |
| idx++; | |
| } | |
| if (result.is_dxil) | |
| result.wide_name.reserve(idx); | |
| else | |
| result.name.reserve(idx); | |
| /* Copy profile name to relevant string */ | |
| for (uint32_t i = 0; i < idx; i++) | |
| { | |
| if (result.is_dxil) | |
| result.wide_name.push_back(str[i]); | |
| else | |
| result.name.push_back(char(str[i])); | |
| } | |
| return std::make_optional(std::move(result)); | |
| } | |
| template<typename string_type> | |
| void add_argument(const string_type &str) | |
| { | |
| /* Handle spaces as delimiters within compiler arguments */ | |
| using char_type = typename string_type::value_type; | |
| string_type prefix; | |
| prefix = char_type('-'); | |
| auto delim = str.find(' '); | |
| arguments.push_back(prefix + str.substr(0, delim)); | |
| while (delim != string_type::npos) | |
| { | |
| auto next = str.find(' ', delim + 1); | |
| arguments.push_back(str.substr(delim + 1, next - delim - 1)); | |
| delim = next; | |
| } | |
| } | |
| }; | |
| struct work_item | |
| { | |
| std::filesystem::path in_file; | |
| std::filesystem::path out_file; | |
| /* Support one DXIL profile and one DXBC one */ | |
| std::array<shader_profile, 2> profiles; | |
| uint32_t profile_count = 0; | |
| static std::optional<work_item> parse(std::filesystem::path file, | |
| const std::filesystem::path &dst_dir) | |
| { | |
| work_item result; | |
| /* Get rid of .hlsl extension right away */ | |
| auto file_name = file.filename().stem().native(); | |
| /* File names are formatted as: | |
| * frog.vs_6_6.enable-16-bit-types.vs_5_1.hlsl.Gis.hlsl | |
| * | |
| * where 'frog' is used as the output file name, vs_x_y are the shader | |
| * profiles to compile the shader for, and all other parts are interpreted | |
| * as compiler options for the previously specified profile. */ | |
| auto delim = file_name.find('.'); | |
| result.out_file = dst_dir / file_name.substr(0, delim); | |
| result.out_file += ".h"; | |
| /* Parse the file name as a sequence of shader targets delimited | |
| * by dots, e.g. frog.vs_6_6.vs_5_1.hlsl */ | |
| while (delim != std::filesystem::path::string_type::npos) | |
| { | |
| auto next = file_name.find('.', delim + 1); | |
| auto substr = file_name.substr(delim + 1, next - delim - 1); | |
| auto profile = shader_profile::parse(substr); | |
| if (profile) | |
| { | |
| if (result.profile_count == result.profiles.size()) | |
| { | |
| std::wcerr << file << ": Too many profiles specified." << std::endl; | |
| return std::nullopt; | |
| } | |
| result.profiles[result.profile_count++] = std::move(*profile); | |
| } | |
| else | |
| { | |
| if (!result.profile_count) | |
| { | |
| std::wcerr << file << ": Found option " << substr << ", but no profile specified." << std::endl; | |
| return std::nullopt; | |
| } | |
| result.profiles[result.profile_count - 1].add_argument(substr); | |
| } | |
| delim = next; | |
| } | |
| if (!result.profile_count) | |
| { | |
| std::wcerr << file << ": No profiles specified." << std::endl; | |
| return std::nullopt; | |
| } | |
| result.in_file = std::move(file); | |
| return std::make_optional(std::move(result)); | |
| } | |
| bool needs_update() const | |
| { | |
| /* Check if the output file exists first, then compare access dates */ | |
| if (!std::filesystem::is_regular_file(out_file)) | |
| return true; | |
| auto in_time = std::filesystem::last_write_time(in_file); | |
| auto out_time = std::filesystem::last_write_time(out_file); | |
| return in_time > out_time; | |
| } | |
| bool compile(const compiler_instances &compilers) const | |
| { | |
| if (!create_output_directory(out_file.parent_path())) | |
| return false; | |
| /* Read entire source file into a character array */ | |
| std::ifstream in_stream(in_file, std::ios::binary | std::ios::ate); | |
| if (!in_stream.is_open()) | |
| { | |
| std::wcerr << in_file << ": Failed to open file." << std::endl; | |
| return false; | |
| } | |
| std::vector<char> code(in_stream.tellg()); | |
| in_stream.seekg(0, std::ios::beg); | |
| if (!in_stream.read(code.data(), code.size())) | |
| { | |
| std::wcerr << in_file << ": Failed to read file." << std::endl; | |
| return false; | |
| } | |
| /* Source file name, used by both DXC and d3dcompiler for | |
| * error reporting, but in different character encodings */ | |
| auto source_name_wide = in_file.filename().native(); | |
| auto source_name = from_wide_string(source_name_wide); | |
| /* Accumulate data in a string stream first, so that we can | |
| * discard it and not alter the output file's time stamp in | |
| * case any profile fails to compile. */ | |
| std::stringstream header_stream; | |
| for (uint32_t i = 0; i < profile_count; i++) | |
| { | |
| std::vector<char> binary; | |
| bool status; | |
| if (profiles[i].is_dxil) | |
| status = compilers.dxc.compile(code, in_file.filename().c_str(), | |
| profiles[i].wide_name.data(), L"main", profiles[i].arguments, binary); | |
| else | |
| status = compilers.d3dcompiler.compile(code, source_name.c_str(), | |
| profiles[i].name.data(), "main", profiles[i].arguments, binary); | |
| if (!status) | |
| return false; | |
| emit_binary(header_stream, binary, profiles[i].is_dxil); | |
| } | |
| /* Compiling will have succeeded at this point, write the output. */ | |
| std::ofstream out_stream(out_file, std::ios::trunc | std::ios::binary); | |
| if (!out_stream.is_open()) | |
| { | |
| std::wcerr << out_file << ": Failed to create file." << std::endl; | |
| return false; | |
| } | |
| if (!(out_stream << header_stream.str())) | |
| { | |
| std::wcerr << out_file << ": Failed to write file." << std::endl; | |
| return false; | |
| } | |
| return true; | |
| } | |
| private: | |
| void emit_binary(std::ostream &out_stream, const std::vector<char> &binary, bool is_dxil) const | |
| { | |
| const char *var_suffix = is_dxil ? "dxil" : "dxbc"; | |
| std::string var_name = from_wide_string(out_file.stem().native()); | |
| out_stream << "static const " << (is_dxil ? "BYTE" : "DWORD") | |
| << " " << var_name << "_code_" << var_suffix << "[] =\n{\n"; | |
| /* Stick to the usual formatting */ | |
| size_t unit_size = is_dxil ? 1 : 4; | |
| size_t row_length = 32; | |
| for (size_t i = 0; i < binary.size(); i += row_length) | |
| { | |
| out_stream << " "; | |
| for (size_t j = 0; j < row_length / unit_size; j++) | |
| { | |
| size_t offset = i + j * unit_size; | |
| /* Handle lines that are less than the maximum length */ | |
| if (offset >= binary.size()) | |
| break; | |
| uint32_t unit = 0u; | |
| std::memcpy(&unit, &binary[offset], unit_size); | |
| out_stream << " 0x" << std::hex << std::setfill('0') << std::setw(2 * unit_size) << unit << ","; | |
| } | |
| out_stream << "\n"; | |
| } | |
| out_stream << "};\n"; | |
| out_stream << "#ifdef __GNUC__\n"; | |
| out_stream << "#define UNUSED_ARRAY_ATTR __attribute__((unused))\n"; | |
| out_stream << "#else\n"; | |
| out_stream << "#define UNUSED_ARRAY_ATTR\n"; | |
| out_stream << "#endif\n"; | |
| out_stream << "UNUSED_ARRAY_ATTR static const D3D12_SHADER_BYTECODE " << var_name << "_" << var_suffix | |
| << " = { " << var_name << "_code_" << var_suffix << ", " | |
| << "sizeof(" << var_name << "_code_" << var_suffix << ") };\n"; | |
| out_stream << "#undef UNUSED_ARRAY_ATTR\n"; | |
| } | |
| }; | |
| /* Simple worker thread system that spawns up to one thread | |
| * per available CPU core on demand in case a large number | |
| * of files needs updating. */ | |
| class thread_manager | |
| { | |
| public: | |
| thread_manager() | |
| : m_max_threads(std::thread::hardware_concurrency()) | |
| { | |
| m_threads.reserve(m_max_threads); | |
| } | |
| /* Queues up work item and spawns a thread if necessary */ | |
| void add_item(work_item &&item) | |
| { | |
| if (m_threads.size() < m_max_threads) | |
| m_threads.emplace_back([this] { run(); }); | |
| std::lock_guard lock(m_mutex); | |
| m_work_items.push(std::move(item)); | |
| m_cond.notify_one(); | |
| } | |
| /* Waits for all threads to finish and returns false | |
| * if any queued up compile job has failed. */ | |
| bool stop() | |
| { | |
| { | |
| std::lock_guard lock(m_mutex); | |
| m_stop = true; | |
| m_cond.notify_all(); | |
| } | |
| for (auto &t : m_threads) | |
| t.join(); | |
| m_threads.clear(); | |
| return m_status.load(); | |
| } | |
| private: | |
| compiler_instances m_compilers; | |
| std::atomic<bool> m_status = { true }; | |
| std::mutex m_mutex; | |
| std::condition_variable m_cond; | |
| std::queue<work_item> m_work_items; | |
| std::vector<std::thread> m_threads; | |
| bool m_stop = false; | |
| uint32_t m_max_threads = 0; | |
| void run() | |
| { | |
| while (true) | |
| { | |
| std::unique_lock lock(m_mutex); | |
| m_cond.wait(lock, [this] | |
| { | |
| return m_stop || !m_work_items.empty(); | |
| }); | |
| /* Ensure queue is drained before exiting */ | |
| if (m_work_items.empty()) | |
| return; | |
| work_item item = std::move(m_work_items.front()); | |
| m_work_items.pop(); | |
| lock.unlock(); | |
| if (!item.compile(m_compilers)) | |
| m_status.store(false); | |
| } | |
| } | |
| }; | |
| /* Scans shader files in the given source directory and queues up any | |
| * file for compiling if it has been updated more recently than its | |
| * corresponding output file. */ | |
| bool build_shader_files(const std::filesystem::path &src_dir, | |
| const std::filesystem::path &dst_dir, bool rebuild_all) | |
| { | |
| thread_manager threads; | |
| bool error = false; | |
| for (const auto &file : std::filesystem::directory_iterator(src_dir)) | |
| { | |
| if (!file.is_regular_file()) | |
| continue; | |
| const auto &file_path = file.path(); | |
| if (file_path.extension() != ".hlsl") | |
| continue; | |
| auto item = work_item::parse(file_path, dst_dir); | |
| if (!item) | |
| { | |
| error = true; | |
| continue; | |
| } | |
| if (rebuild_all || item->needs_update()) | |
| threads.add_item(std::move(*item)); | |
| } | |
| return threads.stop() && !error; | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| std::filesystem::path src_dir = std::filesystem::current_path(); | |
| std::filesystem::path dst_dir; | |
| bool rebuild_all = false; | |
| for (int i = 1; i < argc; i++) | |
| { | |
| if (!std::strcmp(argv[i], "-a")) | |
| { | |
| rebuild_all = true; | |
| } | |
| else if (!std::strcmp(argv[i], "-t")) | |
| { | |
| if (i + 1 >= argc) | |
| { | |
| std::wcerr << "-t: No target directory specified." << std::endl; | |
| return 1; | |
| } | |
| dst_dir = argv[++i]; | |
| } | |
| else if (!std::strcmp(argv[i], "--help")) | |
| { | |
| std::wcout << "Usage: " << argv[0] << " [-a] [-t output_dir] [input_dir]" << std::endl; | |
| return 0; | |
| } | |
| else if (argv[i][0] != '-') | |
| { | |
| src_dir = argv[i]; | |
| } | |
| } | |
| if (dst_dir.empty()) | |
| dst_dir = src_dir / "headers"; | |
| if (!std::filesystem::is_directory(src_dir)) | |
| { | |
| std::wcerr << "Input directory '" << src_dir << "' does not exist." << std::endl; | |
| return 1; | |
| } | |
| if (!create_output_directory(dst_dir)) | |
| { | |
| std::wcerr << "Failed to create target directory '" << dst_dir << "'." << std::endl; | |
| return 1; | |
| } | |
| return build_shader_files(src_dir, dst_dir, rebuild_all) ? 0 : 1; | |
| } |