ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
shader_library.cpp
Go to the documentation of this file.
1#include "shader_library.hpp"
2
4
5#include <mxvk/mxvk.hpp>
6#include <opencv2/core.hpp>
7
8#include <algorithm>
9#include <atomic>
10#include <cctype>
11#include <chrono>
12#include <cstring>
13#include <exception>
14#include <fstream>
15#include <iomanip>
16#include <iostream>
17#include <limits>
18#include <mutex>
19#if defined(__linux__) || defined(__APPLE__)
20#include <cerrno>
21#include <spawn.h>
22#include <sys/wait.h>
23#include <unistd.h>
24#endif
25#include <stdexcept>
26#include <thread>
27#include <unordered_set>
28
29#ifdef _WIN32
30#ifndef NOMINMAX
31#define NOMINMAX
32#endif
33#include <windows.h>
34#endif
35
36#if defined(__linux__) || defined(__APPLE__)
37extern char **environ;
38#endif
39
40namespace acmxvk {
41 [[nodiscard]] bool isValidCustomUniformName(const std::string &name) {
42 if (name.starts_with("gl_")) {
43 return false;
44 }
45 try {
47 "custom uniform name");
48 return true;
49 } catch (const std::runtime_error &) {
50 return false;
51 }
52 }
53
54 [[nodiscard]] ShaderManifest loadShaderManifest(const fs::path &directory) {
55 ShaderManifest manifest;
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;
60 input::validate_text_file(json_path, "shader library.json");
61 try {
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: " +
67 json_path.string());
68 }
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");
74 }
75 for (const cv::FileNode &entry : shader_entries) {
76 if (manifest.entries.size() >=
78 throw std::runtime_error(
79 json_path.string() +
80 " contains too many shader entries");
81 }
82 std::string filename;
83 if (entry.isString()) {
84 entry >> filename;
85 } else if (entry.isMap() && !entry["file"].empty()) {
86 entry["file"] >> filename;
87 } else {
88 throw std::runtime_error(
89 json_path.string() +
90 " contains a shader entry without a file name");
91 }
92 filename = trim(std::move(filename));
93 if (filename.empty()) {
94 throw std::runtime_error(
95 json_path.string() +
96 " contains a shader entry without a file name");
97 }
100 json_path.string() + " shader file");
101 manifest.entries.push_back(std::move(filename));
102 }
103
104 const cv::FileNode custom_uniforms = storage["custom_uniforms"];
105 if (!custom_uniforms.empty()) {
106 if (!custom_uniforms.isMap()) {
107 throw std::runtime_error(
108 json_path.string() +
109 " field 'custom_uniforms' must be an object");
110 }
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) {
116 if (manifest.custom_uniforms.size() >=
117 mxvk::VK_Sprite::MAX_CUSTOM_UNIFORMS) {
118 throw std::runtime_error(
119 json_path.string() +
120 " contains more than " +
121 std::to_string(mxvk::VK_Sprite::MAX_CUSTOM_UNIFORMS) +
122 " custom uniforms");
123 }
124
125 const cv::FileNode entry = *iterator;
127 uniform.name = entry.name();
128 if (!entry.isMap() ||
129 !isValidCustomUniformName(uniform.name)) {
130 throw std::runtime_error(
131 json_path.string() +
132 " contains an invalid custom uniform: " +
133 uniform.name);
134 }
135 uniform.slot = manifest.custom_uniforms.size();
136 if (!entry["slot"].empty()) {
137 int slot = -1;
138 entry["slot"] >> slot;
139 if (slot < 0 ||
140 slot >= static_cast<int>(
141 mxvk::VK_Sprite::MAX_CUSTOM_UNIFORMS)) {
142 throw std::runtime_error(
143 json_path.string() +
144 " contains an invalid slot for custom uniform: " +
145 uniform.name);
146 }
147 uniform.slot = static_cast<std::size_t>(slot);
148 if (!occupied_slots.insert(uniform.slot).second) {
149 throw std::runtime_error(
150 json_path.string() +
151 " assigns more than one custom uniform to slot " +
152 std::to_string(slot));
153 }
154 has_explicit_slots = true;
155 } else {
156 has_implicit_slots = true;
157 }
158 if (!entry["minimum"].empty()) {
159 entry["minimum"] >> uniform.minimum;
160 }
161 if (!entry["maximum"].empty()) {
162 entry["maximum"] >> uniform.maximum;
163 }
164 if (!entry["step"].empty()) {
165 entry["step"] >> uniform.step;
166 }
167 uniform.value = uniform.minimum;
168 if (!entry["value"].empty()) {
169 entry["value"] >> uniform.value;
170 }
171 if (!std::isfinite(uniform.minimum) ||
172 !std::isfinite(uniform.maximum) ||
173 !std::isfinite(uniform.step) ||
174 !std::isfinite(uniform.value) ||
175 uniform.maximum <= uniform.minimum ||
176 uniform.step <= 0.0 ||
177 std::abs(uniform.minimum) >
178 std::numeric_limits<float>::max() ||
179 std::abs(uniform.maximum) >
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(
186 json_path.string() +
187 " contains an invalid range for custom uniform: " +
188 uniform.name);
189 }
190 uniform.value = std::clamp(
191 uniform.value, uniform.minimum, uniform.maximum);
192 manifest.custom_uniforms.push_back(std::move(uniform));
193 }
194 if (has_explicit_slots && has_implicit_slots) {
195 throw std::runtime_error(
196 json_path.string() +
197 " must specify a slot for every custom uniform or none");
198 }
199 if (has_explicit_slots) {
200 std::sort(manifest.custom_uniforms.begin(),
201 manifest.custom_uniforms.end(),
202 [](const ShaderManifest::CustomUniform &left,
203 const ShaderManifest::CustomUniform &right) {
204 return left.slot < right.slot;
205 });
206 for (std::size_t slot = 0;
207 slot < manifest.custom_uniforms.size(); ++slot) {
208 if (manifest.custom_uniforms[slot].slot != slot) {
209 throw std::runtime_error(
210 json_path.string() +
211 " custom uniform slots must be contiguous from zero");
212 }
213 }
214 }
215 }
216 } catch (const cv::Exception &error) {
217 throw std::runtime_error("unable to parse shader manifest " +
218 json_path.string() + ": " + error.what());
219 }
220 return manifest;
221 }
222
223 if (!fs::is_regular_file(text_path)) {
224 throw std::runtime_error("no library.json or index.txt found in shader library: " +
225 directory.string());
226 }
227 manifest.path = text_path;
228 input::validate_file_size(text_path, "shader index.txt");
229 std::ifstream manifest_input(text_path);
230 if (!manifest_input) {
231 throw std::runtime_error("unable to open shader manifest: " +
232 text_path.string());
233 }
234 std::string line;
235 std::size_t line_number = 1;
236 while (input::read_bounded_line(manifest_input, line,
237 "shader index.txt", line_number++)) {
238 line = trim(std::move(line));
239 if (!line.empty() && line.front() != '#') {
240 if (manifest.entries.size() >=
242 throw std::runtime_error(
243 text_path.string() +
244 " contains too many shader entries");
245 }
248 text_path.string() + " shader file");
249 manifest.entries.push_back(std::move(line));
250 }
251 }
252 return manifest;
253 }
254
255 [[nodiscard]] fs::path resolveShaderManifestEntry(const fs::path &directory,
256 std::string entry) {
257 std::replace(entry.begin(), entry.end(), '\\', '/');
258 const fs::path relative_path(entry);
259 if (relative_path.is_absolute()) {
260 return {};
261 }
262
263 const fs::path normalized = relative_path.lexically_normal();
264 const std::string normalized_text = normalized.generic_string();
265 if (normalized_text.empty() || normalized_text == "." ||
266 normalized_text == ".." || normalized_text.starts_with("../") ||
267 normalized_text.find("/../") != std::string::npos ||
268 normalized.extension() != ".spv") {
269 return {};
270 }
271
272 std::error_code error;
273 const fs::path root = fs::weakly_canonical(directory, error);
274 if (error) {
275 return {};
276 }
277 const fs::path shader = fs::weakly_canonical(root / normalized, error);
278 if (error || !fs::is_regular_file(shader)) {
279 return {};
280 }
281 const std::string resolved_relative = shader.lexically_relative(root).generic_string();
282 if (resolved_relative.empty() || resolved_relative == ".." ||
283 resolved_relative.starts_with("../")) {
284 return {};
285 }
286 return shader;
287 }
288
289 [[nodiscard]] fs::path resolveShaderBuildEntry(const fs::path &directory,
290 std::string entry) {
291 std::replace(entry.begin(), entry.end(), '\\', '/');
292 const fs::path relative_path(entry);
293 if (relative_path.is_absolute()) {
294 return {};
295 }
296
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")) {
305 return {};
306 }
307
308 std::error_code error;
309 const fs::path root = fs::weakly_canonical(directory, error);
310 if (error) {
311 return {};
312 }
313 const fs::path source = fs::weakly_canonical(root / normalized, error);
314 if (error || !fs::is_regular_file(source)) {
315 return {};
316 }
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("../")) {
321 return {};
322 }
323 return source;
324 }
325
326 [[nodiscard]] std::string escapeJson(std::string_view value) {
327 std::ostringstream escaped;
328 for (const unsigned char character : value) {
329 switch (character) {
330 case '"':
331 escaped << "\\\"";
332 break;
333 case '\\':
334 escaped << "\\\\";
335 break;
336 case '\b':
337 escaped << "\\b";
338 break;
339 case '\f':
340 escaped << "\\f";
341 break;
342 case '\n':
343 escaped << "\\n";
344 break;
345 case '\r':
346 escaped << "\\r";
347 break;
348 case '\t':
349 escaped << "\\t";
350 break;
351 default:
352 if (character < 0x20U) {
353 escaped << "\\u" << std::hex << std::uppercase
354 << std::setw(4) << std::setfill('0')
355 << static_cast<unsigned int>(character)
356 << std::dec << std::nouppercase;
357 } else {
358 escaped << static_cast<char>(character);
359 }
360 break;
361 }
362 }
363 return escaped.str();
364 }
365
366 [[nodiscard]] fs::path temporaryBuildPath(const fs::path &destination) {
367 static std::atomic<std::uint64_t> sequence{0};
368 for (int attempt = 0; attempt < 100; ++attempt) {
369 fs::path temporary = destination;
370 temporary += ".acmxvk-tmp-" +
371 std::to_string(std::chrono::steady_clock::now()
372 .time_since_epoch()
373 .count()) +
374 "-" +
375 std::to_string(sequence.fetch_add(1U) + 1U);
376 if (!fs::exists(temporary)) {
377 return temporary;
378 }
379 }
380 throw std::runtime_error(
381 "unable to allocate a temporary shader build path for: " +
382 destination.string());
383 }
384
385 void replaceBuiltFile(const fs::path &temporary,
386 const fs::path &destination) {
387 std::error_code error;
388 fs::rename(temporary, destination, error);
389 if (error) {
390 fs::remove(temporary);
391 throw std::runtime_error("unable to install built file " +
392 destination.string() + ": " +
393 error.message());
394 }
395 }
396
397 class ShaderCompilationError : public std::runtime_error {
398 public:
399 using std::runtime_error::runtime_error;
400 };
401
402#ifdef _WIN32
403 [[nodiscard]] std::wstring utf8_to_wide(const std::string &value) {
404 if (value.empty()) {
405 return {};
406 }
407 const int length = MultiByteToWideChar(
408 CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
409 static_cast<int>(value.size()), nullptr, 0);
410 if (length <= 0) {
411 throw std::runtime_error("invalid UTF-8 in Windows command argument");
412 }
413 std::wstring result(static_cast<std::size_t>(length), L'\0');
414 if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
415 static_cast<int>(value.size()), result.data(),
416 length) != length) {
417 throw std::runtime_error("unable to convert Windows command argument");
418 }
419 return result;
420 }
421
422 [[nodiscard]] std::wstring
423 quote_windows_argument(const std::wstring &value) {
424 std::wstring quoted{L"\""};
425 std::size_t backslash_count = 0;
426 for (const wchar_t character : value) {
427 if (character == L'\\') {
428 ++backslash_count;
429 continue;
430 }
431 if (character == L'"') {
432 quoted.append(backslash_count * 2U + 1U, L'\\');
433 quoted += character;
434 backslash_count = 0;
435 continue;
436 }
437 quoted.append(backslash_count, L'\\');
438 backslash_count = 0;
439 quoted += character;
440 }
441 quoted.append(backslash_count * 2U, L'\\');
442 quoted += L'"';
443 return quoted;
444 }
445
446 [[nodiscard]] DWORD run_windows_process(
447 const std::vector<std::wstring> &arguments) {
448 std::wstring command_line;
449 for (const std::wstring &argument : arguments) {
450 if (!command_line.empty()) {
451 command_line += L' ';
452 }
453 command_line += quote_windows_argument(argument);
454 }
455
456 STARTUPINFOW startup_info{};
457 startup_info.cb = sizeof(startup_info);
458 PROCESS_INFORMATION process_info{};
459 if (CreateProcessW(nullptr, command_line.data(), nullptr, nullptr,
460 TRUE, 0, nullptr, nullptr, &startup_info,
461 &process_info) == FALSE) {
462 const DWORD process_error = GetLastError();
463 throw std::runtime_error(
464 "unable to execute glslc (Windows error " +
465 std::to_string(process_error) + ")");
466 }
467
468 CloseHandle(process_info.hThread);
469 const DWORD wait_result =
470 WaitForSingleObject(process_info.hProcess, INFINITE);
471 DWORD exit_code = 1;
472 if (wait_result != WAIT_OBJECT_0 ||
473 GetExitCodeProcess(process_info.hProcess, &exit_code) == FALSE) {
474 const DWORD process_error = GetLastError();
475 CloseHandle(process_info.hProcess);
476 throw std::runtime_error(
477 "unable to wait for glslc (Windows error " +
478 std::to_string(process_error) + ")");
479 }
480 CloseHandle(process_info.hProcess);
481 return exit_code;
482 }
483#endif
484
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",
490 output.string()};
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());
495 }
496 argument_pointers.push_back(nullptr);
497
498 pid_t process = 0;
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));
505 }
506
507 int status = 0;
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)));
512 }
513 }
514 if (!WIFEXITED(status)) {
515 throw std::runtime_error("glslc terminated by a signal for " +
516 source.string());
517 }
518 if (WEXITSTATUS(status) != 0) {
520 "glslc failed for " + source.string() + " (exit status " +
521 std::to_string(WEXITSTATUS(status)) + ")");
522 }
523#elif defined(_WIN32)
524 const DWORD result = run_windows_process(
525 {utf8_to_wide(executable), L"-I", source_root.wstring(),
526 source.wstring(), L"-o", output.wstring()});
527 if (result != 0U) {
529 "glslc failed for " + source.string() + " (exit status " +
530 std::to_string(result) + ")");
531 }
532#else
533#error Unsupported platform
534#endif
535 }
536
537 [[nodiscard]] int buildShaderLibrary(const Options &options) {
538 const fs::path requested_manifest =
539 fs::absolute(options.build_manifest).lexically_normal();
540 if (requested_manifest.filename() != "library.json") {
541 throw std::runtime_error(
542 "--build must name a file called library.json");
543 }
544 input::validate_text_file(requested_manifest,
545 "source shader library.json");
546
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());
553 }
554 fs::create_directories(options.build_directory, error);
555 if (error) {
556 throw std::runtime_error("unable to create shader build directory: " +
557 error.message());
558 }
559 const fs::path output_root =
560 fs::weakly_canonical(options.build_directory, error);
561 if (error || output_root.empty()) {
562 throw std::runtime_error("unable to resolve shader build directory: " +
563 options.build_directory);
564 }
565 if (source_root == output_root) {
566 throw std::runtime_error(
567 "the shader output directory must differ from the source "
568 "library directory");
569 }
570
571 const ShaderManifest manifest = loadShaderManifest(source_root);
572 if (manifest.entries.empty()) {
573 throw std::runtime_error(
574 "source library.json contains no shader entries");
575 }
576
577 struct PreparedEntry {
578 fs::path relative;
579 std::string output_entry;
580 bool ready = false;
581 };
582
583 std::vector<PreparedEntry> prepared_entries(manifest.entries.size());
584 std::vector<std::string> output_entries_by_index(
585 manifest.entries.size());
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;
598
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"
608 << std::flush;
609 next_progress += 5;
610 }
611 };
612
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);
617 }
618 };
619
620 for (std::size_t index = 0; index < manifest.entries.size(); ++index) {
621 const std::string &entry = manifest.entries[index];
622 try {
623 std::string normalized_entry = entry;
624 std::replace(normalized_entry.begin(), normalized_entry.end(),
625 '\\', '/');
626 fs::path relative(normalized_entry);
627 relative = relative.lexically_normal();
628 if (relative.extension() != ".spv") {
629 relative += ".spv";
630 }
631 const std::string output_entry = relative.generic_string();
632 std::string output_key = output_entry;
633 std::transform(
634 output_key.begin(), output_key.end(), output_key.begin(),
635 [](unsigned char character) {
636 return static_cast<char>(std::tolower(character));
637 });
638 if (!unique_outputs.insert(output_key).second) {
639 throw std::runtime_error(
640 "source library produces a duplicate output path: " +
641 output_entry);
642 }
643 prepared_entries[index] =
644 PreparedEntry{relative, output_entry, true};
645 } catch (const std::exception &failure_value) {
646 if (!options.build_fix) {
647 throw;
648 }
649 ++failed;
650 std::cerr << "acmxvk: fix omitted '" << entry
651 << "': " << failure_value.what() << '\n';
652 report_progress();
653 }
654 }
655
656 const auto process_entry = [&](std::size_t index) {
657 const PreparedEntry &prepared = prepared_entries[index];
658 if (!prepared.ready) {
659 return;
660 }
661 const std::string &entry = manifest.entries[index];
662 fs::path source;
663 fs::path destination;
664 try {
665 source = resolveShaderBuildEntry(source_root, entry);
666 if (source.empty()) {
667 throw std::runtime_error(
668 "source library contains an unavailable or unsafe shader: " +
669 entry);
670 }
671
672 destination = output_root / prepared.relative;
673 std::error_code entry_error;
674 fs::create_directories(destination.parent_path(), entry_error);
675 if (entry_error) {
676 throw std::runtime_error(
677 "unable to create shader output directory: " +
678 entry_error.message());
679 }
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)
685 .generic_string();
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);
692 }
693
694 bool needs_build = !fs::is_regular_file(destination);
695 if (!needs_build) {
696 needs_build = fs::last_write_time(destination, entry_error) <
697 fs::last_write_time(source);
698 if (entry_error) {
699 needs_build = true;
700 entry_error.clear();
701 }
702 }
703 if (!needs_build) {
704 try {
706 destination, "built shader module");
707 } catch (const std::runtime_error &) {
708 needs_build = true;
709 }
710 }
711
712 if (needs_build) {
713 const fs::path temporary = temporaryBuildPath(destination);
714 const bool copy_source = source.extension() == ".spv";
715 try {
716 if (copy_source) {
718 source, "source shader module");
719 fs::copy_file(
720 source, temporary,
721 fs::copy_options::overwrite_existing);
722 } else {
724 "GLSL shader source");
725 runGlslc(options.glslc_executable, source_root,
726 source, temporary);
727 }
729 "compiled shader module");
730 replaceBuiltFile(temporary, destination);
731 } catch (...) {
732 fs::remove(temporary);
733 throw;
734 }
735 if (copy_source) {
736 ++copied;
737 } else {
738 ++compiled;
739 }
740 } else {
741 ++current;
742 }
743 output_entries_by_index[index] = prepared.output_entry;
744 } catch (const std::exception &failure_value) {
745 if (!options.build_fix) {
746 store_failure(std::current_exception());
747 } else {
748 try {
749 const bool compilation_failed =
750 dynamic_cast<const ShaderCompilationError *>(
751 &failure_value) != nullptr;
752 if (!destination.empty()) {
753 std::error_code remove_error;
754 fs::remove(destination, remove_error);
755 if (remove_error) {
756 throw std::runtime_error(
757 "unable to remove failed shader output " +
758 destination.string() + ": " +
759 remove_error.message());
760 }
761 }
762 if (options.build_prune && compilation_failed &&
763 !source.empty() &&
764 (source.extension() == ".frag" ||
765 source.extension() == ".comp")) {
766 std::error_code remove_error;
767 const bool removed =
768 fs::remove(source, remove_error);
769 if (remove_error || !removed) {
770 throw std::runtime_error(
771 "unable to prune failed shader source " +
772 source.string() +
773 (remove_error
774 ? ": " + remove_error.message()
775 : ": file was not removed"));
776 }
777 ++pruned;
778 const std::lock_guard lock(error_mutex);
779 std::cerr << "acmxvk: pruned failed source '"
780 << source.string() << "'\n";
781 }
782 ++failed;
783 const std::lock_guard lock(error_mutex);
784 std::cerr << "acmxvk: fix omitted '" << entry
785 << "': " << failure_value.what() << '\n';
786 } catch (...) {
787 store_failure(std::current_exception());
788 }
789 }
790 }
791 report_progress();
792 };
793
794 const std::size_t worker_count = std::min(
795 static_cast<std::size_t>(options.build_parallel),
796 manifest.entries.size());
797 if (worker_count > 1U) {
798 std::cout << "acmxvk: building shader library with "
799 << worker_count << " parallel jobs\n";
800 }
801 std::atomic<std::size_t> next_entry{0};
802 const auto worker = [&] {
803 while (true) {
804 const std::size_t index = next_entry.fetch_add(1U);
805 if (index >= manifest.entries.size()) {
806 return;
807 }
808 process_entry(index);
809 }
810 };
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);
815 }
816 for (std::thread &thread : workers) {
817 thread.join();
818 }
819 if (first_failure) {
820 std::rethrow_exception(first_failure);
821 }
822
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));
828 }
829 }
830
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");
835 }
836 const fs::path temporary_manifest =
837 temporaryBuildPath(output_manifest);
838 {
839 std::ofstream output(temporary_manifest,
840 std::ios::out | std::ios::trunc);
841 if (!output) {
842 throw std::runtime_error(
843 "unable to create output library.json");
844 }
845 output << "{\n \"version\": 1"
846 << ",\n \"backend\": \"acmxvk\""
847 << ",\n \"library_type\": \"runtime\"";
848 if (!manifest.custom_uniforms.empty()) {
849 output << ",\n \"custom_uniforms\": {\n";
850 for (std::size_t index = 0;
851 index < manifest.custom_uniforms.size(); ++index) {
852 const ShaderManifest::CustomUniform &uniform =
853 manifest.custom_uniforms[index];
854 output << " \"" << escapeJson(uniform.name)
855 << "\": {\n"
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
862 << "\n }";
863 output << (index + 1U < manifest.custom_uniforms.size()
864 ? ",\n"
865 : "\n");
866 }
867 output << " }";
868 }
869 output << ",\n \"shaders\": [\n";
870 for (std::size_t index = 0; index < output_entries.size();
871 ++index) {
872 output << " \"" << escapeJson(output_entries[index])
873 << '"'
874 << (index + 1U < output_entries.size() ? ",\n"
875 : "\n");
876 }
877 output << " ]\n}\n";
878 if (!output) {
879 fs::remove(temporary_manifest);
880 throw std::runtime_error(
881 "unable to write output library.json");
882 }
883 }
884 try {
885 input::validate_text_file(temporary_manifest,
886 "built shader library.json");
887 replaceBuiltFile(temporary_manifest, output_manifest);
888 } catch (...) {
889 fs::remove(temporary_manifest);
890 throw;
891 }
892
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()
898 << " included\n";
899 return EXIT_SUCCESS;
900 }
901} // namespace acmxvk
GLsizei GLsizei * length
bool read_bounded_line(std::istream &input, std::string &line, std::string_view context, std::size_t line_number, std::size_t maximum_bytes)
constexpr std::size_t MAX_SHADER_ENTRIES
void validate_file_size(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes)
void validate_spirv_file(const std::filesystem::path &path, std::string_view context)
void validate_text_file(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes, std::size_t maximum_line_bytes)
void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty)
fs::path resolveShaderManifestEntry(const fs::path &directory, std::string entry)
std::string escapeJson(std::string_view value)
void runGlslc(const std::string &executable, const fs::path &source_root, const fs::path &source, const fs::path &output)
std::string trim(std::string text)
Definition options.cpp:1619
ShaderManifest loadShaderManifest(const fs::path &directory)
fs::path resolveShaderBuildEntry(const fs::path &directory, std::string entry)
int buildShaderLibrary(const Options &options)
fs::path temporaryBuildPath(const fs::path &destination)
bool isValidCustomUniformName(const std::string &name)
void replaceBuiltFile(const fs::path &temporary, const fs::path &destination)
std::string glslc_executable
Definition options.hpp:173
std::string build_manifest
Definition options.hpp:171
std::string build_directory
Definition options.hpp:172
std::vector< std::string > entries
std::vector< CustomUniform > custom_uniforms