56 const fs::path json_path = directory /
"library.json";
57 const fs::path text_path = directory /
"index.txt";
58 if (fs::is_regular_file(json_path)) {
59 manifest.
path = json_path;
62 cv::FileStorage storage(json_path.string(),
63 cv::FileStorage::READ |
64 cv::FileStorage::FORMAT_JSON);
65 if (!storage.isOpened()) {
66 throw std::runtime_error(
"unable to open shader manifest: " +
69 const cv::FileNode shader_entries = storage[
"shaders"];
70 if (shader_entries.type() == cv::FileNode::NONE ||
71 !shader_entries.isSeq()) {
72 throw std::runtime_error(json_path.string() +
73 " must contain a 'shaders' array");
75 for (
const cv::FileNode &entry : shader_entries) {
78 throw std::runtime_error(
80 " contains too many shader entries");
83 if (entry.isString()) {
85 }
else if (entry.isMap() && !entry[
"file"].empty()) {
86 entry[
"file"] >> filename;
88 throw std::runtime_error(
90 " contains a shader entry without a file name");
92 filename =
trim(std::move(filename));
93 if (filename.empty()) {
94 throw std::runtime_error(
96 " contains a shader entry without a file name");
100 json_path.string() +
" shader file");
101 manifest.
entries.push_back(std::move(filename));
104 const cv::FileNode custom_uniforms = storage[
"custom_uniforms"];
105 if (!custom_uniforms.empty()) {
106 if (!custom_uniforms.isMap()) {
107 throw std::runtime_error(
109 " field 'custom_uniforms' must be an object");
111 bool has_explicit_slots =
false;
112 bool has_implicit_slots =
false;
113 std::unordered_set<std::size_t> occupied_slots;
114 for (
auto iterator = custom_uniforms.begin();
115 iterator != custom_uniforms.end(); ++iterator) {
117 mxvk::VK_Sprite::MAX_CUSTOM_UNIFORMS) {
118 throw std::runtime_error(
120 " contains more than " +
121 std::to_string(mxvk::VK_Sprite::MAX_CUSTOM_UNIFORMS) +
125 const cv::FileNode entry = *iterator;
127 uniform.
name = entry.name();
128 if (!entry.isMap() ||
130 throw std::runtime_error(
132 " contains an invalid custom uniform: " +
136 if (!entry[
"slot"].empty()) {
138 entry[
"slot"] >> slot;
140 slot >=
static_cast<int>(
141 mxvk::VK_Sprite::MAX_CUSTOM_UNIFORMS)) {
142 throw std::runtime_error(
144 " contains an invalid slot for custom uniform: " +
147 uniform.
slot =
static_cast<std::size_t
>(slot);
148 if (!occupied_slots.insert(uniform.
slot).second) {
149 throw std::runtime_error(
151 " assigns more than one custom uniform to slot " +
152 std::to_string(slot));
154 has_explicit_slots =
true;
156 has_implicit_slots =
true;
158 if (!entry[
"minimum"].empty()) {
159 entry[
"minimum"] >> uniform.
minimum;
161 if (!entry[
"maximum"].empty()) {
162 entry[
"maximum"] >> uniform.
maximum;
164 if (!entry[
"step"].empty()) {
165 entry[
"step"] >> uniform.
step;
168 if (!entry[
"value"].empty()) {
169 entry[
"value"] >> uniform.
value;
171 if (!std::isfinite(uniform.
minimum) ||
172 !std::isfinite(uniform.
maximum) ||
173 !std::isfinite(uniform.
step) ||
174 !std::isfinite(uniform.
value) ||
176 uniform.
step <= 0.0 ||
178 std::numeric_limits<float>::max() ||
180 std::numeric_limits<float>::max() ||
181 std::abs(uniform.
step) >
182 std::numeric_limits<float>::max() ||
183 std::abs(uniform.
value) >
184 std::numeric_limits<float>::max()) {
185 throw std::runtime_error(
187 " contains an invalid range for custom uniform: " +
190 uniform.
value = std::clamp(
194 if (has_explicit_slots && has_implicit_slots) {
195 throw std::runtime_error(
197 " must specify a slot for every custom uniform or none");
199 if (has_explicit_slots) {
204 return left.slot < right.slot;
206 for (std::size_t slot = 0;
209 throw std::runtime_error(
211 " custom uniform slots must be contiguous from zero");
216 }
catch (
const cv::Exception &error) {
217 throw std::runtime_error(
"unable to parse shader manifest " +
218 json_path.string() +
": " + error.what());
223 if (!fs::is_regular_file(text_path)) {
224 throw std::runtime_error(
"no library.json or index.txt found in shader library: " +
227 manifest.
path = text_path;
229 std::ifstream manifest_input(text_path);
230 if (!manifest_input) {
231 throw std::runtime_error(
"unable to open shader manifest: " +
235 std::size_t line_number = 1;
237 "shader index.txt", line_number++)) {
238 line =
trim(std::move(line));
239 if (!line.empty() && line.front() !=
'#') {
242 throw std::runtime_error(
244 " contains too many shader entries");
248 text_path.string() +
" shader file");
249 manifest.
entries.push_back(std::move(line));
291 std::replace(entry.begin(), entry.end(),
'\\',
'/');
292 const fs::path relative_path(entry);
293 if (relative_path.is_absolute()) {
297 const fs::path normalized = relative_path.lexically_normal();
298 const std::string normalized_text = normalized.generic_string();
299 const std::string extension = normalized.extension().string();
300 if (normalized_text.empty() || normalized_text ==
"." ||
301 normalized_text ==
".." || normalized_text.starts_with(
"../") ||
302 normalized_text.find(
"/../") != std::string::npos ||
303 (extension !=
".frag" && extension !=
".comp" &&
304 extension !=
".spv")) {
308 std::error_code error;
309 const fs::path root = fs::weakly_canonical(directory, error);
313 const fs::path source = fs::weakly_canonical(root / normalized, error);
314 if (error || !fs::is_regular_file(source)) {
317 const std::string resolved_relative =
318 source.lexically_relative(root).generic_string();
319 if (resolved_relative.empty() || resolved_relative ==
".." ||
320 resolved_relative.starts_with(
"../")) {
485 void runGlslc(
const std::string &executable,
const fs::path &source_root,
486 const fs::path &source,
const fs::path &output) {
487#if defined(__linux__) || defined(__APPLE__)
488 std::vector<std::string> arguments{
489 executable,
"-I", source_root.string(), source.string(),
"-o",
491 std::vector<char *> argument_pointers;
492 argument_pointers.reserve(arguments.size() + 1U);
493 for (std::string &argument : arguments) {
494 argument_pointers.push_back(argument.data());
496 argument_pointers.push_back(
nullptr);
499 const int spawn_result =
500 posix_spawnp(&process, executable.c_str(),
nullptr,
nullptr,
501 argument_pointers.data(), environ);
502 if (spawn_result != 0) {
503 throw std::runtime_error(
"unable to execute glslc '" + executable +
504 "': " + std::strerror(spawn_result));
508 while (::waitpid(process, &status, 0) < 0) {
509 if (errno != EINTR) {
510 throw std::runtime_error(
"unable to wait for glslc: " +
511 std::string(std::strerror(errno)));
514 if (!WIFEXITED(status)) {
515 throw std::runtime_error(
"glslc terminated by a signal for " +
518 if (WEXITSTATUS(status) != 0) {
520 "glslc failed for " + source.string() +
" (exit status " +
521 std::to_string(WEXITSTATUS(status)) +
")");
524 const DWORD result = run_windows_process(
525 {utf8_to_wide(executable), L
"-I", source_root.wstring(),
526 source.wstring(), L
"-o", output.wstring()});
529 "glslc failed for " + source.string() +
" (exit status " +
530 std::to_string(result) +
")");
533#error Unsupported platform
538 const fs::path requested_manifest =
540 if (requested_manifest.filename() !=
"library.json") {
541 throw std::runtime_error(
542 "--build must name a file called library.json");
545 "source shader library.json");
547 std::error_code error;
548 const fs::path source_root =
549 fs::weakly_canonical(requested_manifest.parent_path(), error);
550 if (error || source_root.empty()) {
551 throw std::runtime_error(
"unable to resolve source shader library: " +
552 requested_manifest.string());
556 throw std::runtime_error(
"unable to create shader build directory: " +
559 const fs::path output_root =
561 if (error || output_root.empty()) {
562 throw std::runtime_error(
"unable to resolve shader build directory: " +
565 if (source_root == output_root) {
566 throw std::runtime_error(
567 "the shader output directory must differ from the source "
568 "library directory");
572 if (manifest.
entries.empty()) {
573 throw std::runtime_error(
574 "source library.json contains no shader entries");
577 struct PreparedEntry {
579 std::string output_entry;
583 std::vector<PreparedEntry> prepared_entries(manifest.
entries.size());
584 std::vector<std::string> output_entries_by_index(
586 std::unordered_set<std::string> unique_outputs;
587 std::atomic<std::size_t> compiled{0};
588 std::atomic<std::size_t> copied{0};
589 std::atomic<std::size_t> current{0};
590 std::atomic<std::size_t> failed{0};
591 std::atomic<std::size_t> pruned{0};
592 std::atomic<std::size_t> processed{0};
593 int next_progress = 5;
594 std::mutex progress_mutex;
595 std::mutex error_mutex;
596 std::mutex failure_mutex;
597 std::exception_ptr first_failure;
599 const auto report_progress = [&] {
600 const std::size_t completed = processed.fetch_add(1U) + 1U;
601 const int percentage =
static_cast<int>(
602 completed * 100U / manifest.
entries.size());
603 const std::lock_guard lock(progress_mutex);
604 while (next_progress <= 100 && percentage >= next_progress) {
605 std::cout <<
"acmxvk: build progress: " << next_progress
606 <<
"% (" << completed <<
'/'
607 << manifest.
entries.size() <<
")\n"
613 const auto store_failure = [&](std::exception_ptr failure) {
614 const std::lock_guard lock(failure_mutex);
615 if (!first_failure) {
616 first_failure = std::move(failure);
620 for (std::size_t index = 0; index < manifest.
entries.size(); ++index) {
621 const std::string &entry = manifest.
entries[index];
623 std::string normalized_entry = entry;
624 std::replace(normalized_entry.begin(), normalized_entry.end(),
626 fs::path relative(normalized_entry);
627 relative = relative.lexically_normal();
628 if (relative.extension() !=
".spv") {
631 const std::string output_entry = relative.generic_string();
632 std::string output_key = output_entry;
634 output_key.begin(), output_key.end(), output_key.begin(),
635 [](
unsigned char character) {
636 return static_cast<char>(std::tolower(character));
638 if (!unique_outputs.insert(output_key).second) {
639 throw std::runtime_error(
640 "source library produces a duplicate output path: " +
643 prepared_entries[index] =
644 PreparedEntry{relative, output_entry,
true};
645 }
catch (
const std::exception &failure_value) {
650 std::cerr <<
"acmxvk: fix omitted '" << entry
651 <<
"': " << failure_value.what() <<
'\n';
656 const auto process_entry = [&](std::size_t index) {
657 const PreparedEntry &prepared = prepared_entries[index];
658 if (!prepared.ready) {
661 const std::string &entry = manifest.
entries[index];
663 fs::path destination;
666 if (source.empty()) {
667 throw std::runtime_error(
668 "source library contains an unavailable or unsafe shader: " +
672 destination = output_root / prepared.relative;
673 std::error_code entry_error;
674 fs::create_directories(destination.parent_path(), entry_error);
676 throw std::runtime_error(
677 "unable to create shader output directory: " +
678 entry_error.message());
680 const fs::path destination_parent =
681 fs::weakly_canonical(destination.parent_path(), entry_error);
682 const std::string parent_relative =
683 entry_error ? std::string{}
684 : destination_parent.lexically_relative(output_root)
686 if (entry_error || parent_relative ==
".." ||
687 parent_relative.starts_with(
"../") ||
688 fs::is_symlink(destination)) {
689 throw std::runtime_error(
690 "shader output resolves outside the output directory: " +
691 prepared.output_entry);
694 bool needs_build = !fs::is_regular_file(destination);
696 needs_build = fs::last_write_time(destination, entry_error) <
697 fs::last_write_time(source);
706 destination,
"built shader module");
707 }
catch (
const std::runtime_error &) {
714 const bool copy_source = source.extension() ==
".spv";
718 source,
"source shader module");
721 fs::copy_options::overwrite_existing);
724 "GLSL shader source");
729 "compiled shader module");
732 fs::remove(temporary);
743 output_entries_by_index[index] = prepared.output_entry;
744 }
catch (
const std::exception &failure_value) {
746 store_failure(std::current_exception());
749 const bool compilation_failed =
751 &failure_value) !=
nullptr;
752 if (!destination.empty()) {
753 std::error_code remove_error;
754 fs::remove(destination, remove_error);
756 throw std::runtime_error(
757 "unable to remove failed shader output " +
758 destination.string() +
": " +
759 remove_error.message());
764 (source.extension() ==
".frag" ||
765 source.extension() ==
".comp")) {
766 std::error_code remove_error;
768 fs::remove(source, remove_error);
769 if (remove_error || !removed) {
770 throw std::runtime_error(
771 "unable to prune failed shader source " +
774 ?
": " + remove_error.message()
775 :
": file was not removed"));
778 const std::lock_guard lock(error_mutex);
779 std::cerr <<
"acmxvk: pruned failed source '"
780 << source.string() <<
"'\n";
783 const std::lock_guard lock(error_mutex);
784 std::cerr <<
"acmxvk: fix omitted '" << entry
785 <<
"': " << failure_value.what() <<
'\n';
787 store_failure(std::current_exception());
794 const std::size_t worker_count = std::min(
797 if (worker_count > 1U) {
798 std::cout <<
"acmxvk: building shader library with "
799 << worker_count <<
" parallel jobs\n";
801 std::atomic<std::size_t> next_entry{0};
802 const auto worker = [&] {
804 const std::size_t index = next_entry.fetch_add(1U);
805 if (index >= manifest.
entries.size()) {
808 process_entry(index);
811 std::vector<std::thread> workers;
812 workers.reserve(worker_count);
813 for (std::size_t index = 0; index < worker_count; ++index) {
814 workers.emplace_back(worker);
816 for (std::thread &thread : workers) {
820 std::rethrow_exception(first_failure);
823 std::vector<std::string> output_entries;
824 output_entries.reserve(manifest.
entries.size());
825 for (std::string &entry : output_entries_by_index) {
826 if (!entry.empty()) {
827 output_entries.push_back(std::move(entry));
831 const fs::path output_manifest = output_root /
"library.json";
832 if (fs::is_symlink(output_manifest)) {
833 throw std::runtime_error(
834 "refusing to replace a symbolic-link output library.json");
836 const fs::path temporary_manifest =
839 std::ofstream output(temporary_manifest,
840 std::ios::out | std::ios::trunc);
842 throw std::runtime_error(
843 "unable to create output library.json");
845 output <<
"{\n \"version\": 1"
846 <<
",\n \"backend\": \"acmxvk\""
847 <<
",\n \"library_type\": \"runtime\"";
849 output <<
",\n \"custom_uniforms\": {\n";
850 for (std::size_t index = 0;
856 << std::setprecision(15)
857 <<
" \"slot\": " << uniform.
slot
858 <<
",\n \"minimum\": " << uniform.
minimum
859 <<
",\n \"maximum\": " << uniform.
maximum
860 <<
",\n \"step\": " << uniform.
step
861 <<
",\n \"value\": " << uniform.
value
869 output <<
",\n \"shaders\": [\n";
870 for (std::size_t index = 0; index < output_entries.size();
872 output <<
" \"" <<
escapeJson(output_entries[index])
874 << (index + 1U < output_entries.size() ?
",\n"
879 fs::remove(temporary_manifest);
880 throw std::runtime_error(
881 "unable to write output library.json");
886 "built shader library.json");
889 fs::remove(temporary_manifest);
893 std::cout <<
"acmxvk: shader library built in " << output_root <<
'\n'
894 <<
"acmxvk: " << compiled <<
" compiled, " << copied
895 <<
" copied, " << current <<
" up to date, "
896 << failed <<
" failed, " << pruned <<
" pruned, "
897 << output_entries.size()