device/shader_compiler.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file shader_compiler.cpp | ||
| 3 | /// @brief Compiling through Slang's API: a session per compile, modules loaded from source. | ||
| 4 | #include "ernie/device/shader_compiler.h" | ||
| 5 | |||
| 6 | #include "ernie/util/profiler.h" | ||
| 7 | |||
| 8 | #include <slang-com-ptr.h> | ||
| 9 | #include <slang.h> | ||
| 10 | |||
| 11 | #include <array> | ||
| 12 | #include <cstring> | ||
| 13 | #include <mutex> | ||
| 14 | #include <stdexcept> | ||
| 15 | |||
| 16 | namespace ernie { | ||
| 17 | namespace { | ||
| 18 | |||
| 19 | 4 | std::string Text(slang::IBlob* blob) { | |
| 20 | 8 | return blob != nullptr ? std::string(static_cast<const char*>(blob->getBufferPointer()), | |
| 21 | 4 | blob->getBufferSize()) | |
| 22 | 8 | : std::string(); | |
| 23 | } | ||
| 24 | |||
| 25 | } // namespace | ||
| 26 | |||
| 27 | 48 | struct ShaderCompiler::Session { | |
| 28 | Slang::ComPtr<slang::IGlobalSession> global; | ||
| 29 | std::mutex mutex; | ||
| 30 | |||
| 31 | /// @return The entry point compiled for the target, as Slang's output blob. | ||
| 32 | 106 | Slang::ComPtr<slang::IBlob> Compile(std::span<const ShaderModule> modules, | |
| 33 | std::string_view entryPoint, SlangCompileTarget format, | ||
| 34 | const char* profile) { | ||
| 35 | 106 | if (modules.empty()) { | |
| 36 | ✗ | throw std::invalid_argument("Compiling no shader modules"); | |
| 37 | } | ||
| 38 | 106 | const std::lock_guard lock(mutex); | |
| 39 | 106 | const Slang::ComPtr<slang::ISession> session = CreateSession(format, profile); | |
| 40 | 106 | slang::IModule* last = LoadModules(*session, modules); | |
| 41 | 211 | return Link(*session, *last, modules.back().name, std::string(entryPoint)); | |
| 42 | 106 | } | |
| 43 | |||
| 44 | private: | ||
| 45 | /// @return A session compiling for the target, the global session started the first time. | ||
| 46 | 106 | Slang::ComPtr<slang::ISession> CreateSession(SlangCompileTarget format, const char* profile) { | |
| 47 | 106 | if (!global && SLANG_FAILED(slang::createGlobalSession(global.writeRef()))) { | |
| 48 | ✗ | throw std::runtime_error("Cannot start the Slang shader compiler"); | |
| 49 | } | ||
| 50 | 106 | const slang::TargetDesc target{.format = format, .profile = global->findProfile(profile)}; | |
| 51 | // Matrices column-major, as glm lays them out, so host structs match without transposes. | ||
| 52 | 106 | std::array<slang::CompilerOptionEntry, 1> options{}; | |
| 53 | 106 | options[0].name = slang::CompilerOptionName::MatrixLayoutColumn; | |
| 54 | 106 | options[0].value.intValue0 = 1; | |
| 55 | // WebGPU has no buffer pointers: ref.slang reads buffers through bound arenas there. | ||
| 56 | 106 | const slang::PreprocessorMacroDesc webGpu{.name = "ERNIE_WEBGPU", .value = "1"}; | |
| 57 | 106 | const bool wgsl = format == SLANG_WGSL; | |
| 58 | 106 | const slang::SessionDesc description{.targets = &target, | |
| 59 | .targetCount = 1, | ||
| 60 | 106 | .preprocessorMacros = wgsl ? &webGpu : nullptr, | |
| 61 | 106 | .preprocessorMacroCount = wgsl ? 1 : 0, | |
| 62 | 106 | .compilerOptionEntries = options.data(), | |
| 63 | .compilerOptionEntryCount = | ||
| 64 | 312 | static_cast<std::uint32_t>(options.size())}; | |
| 65 | 106 | Slang::ComPtr<slang::ISession> session; | |
| 66 | 106 | if (SLANG_FAILED(global->createSession(description, session.writeRef()))) { | |
| 67 | ✗ | throw std::runtime_error("Cannot create a Slang session"); | |
| 68 | } | ||
| 69 | 106 | return session; | |
| 70 | ✗ | } | |
| 71 | |||
| 72 | /// @return The last module, each loaded after the modules it imports. | ||
| 73 | 106 | static slang::IModule* LoadModules(slang::ISession& session, | |
| 74 | std::span<const ShaderModule> modules) { | ||
| 75 | 106 | slang::IModule* last = nullptr; | |
| 76 | 512 | for (const ShaderModule& module : modules) { | |
| 77 | 407 | Slang::ComPtr<slang::IBlob> diagnostics; | |
| 78 | 407 | const std::string path = module.name + ".slang"; | |
| 79 | 407 | last = session.loadModuleFromSourceString( | |
| 80 | module.name.c_str(), path.c_str(), module.source.c_str(), diagnostics.writeRef()); | ||
| 81 | 407 | if (last == nullptr) { | |
| 82 | 2 | throw std::invalid_argument("Shader module " + module.name + | |
| 83 | 4 | " failed to compile:\n" + Text(diagnostics)); | |
| 84 | } | ||
| 85 | 433 | } | |
| 86 | 105 | return last; | |
| 87 | } | ||
| 88 | |||
| 89 | /// @return The code of `module`'s entry point, linked with what it imports. | ||
| 90 | 105 | static Slang::ComPtr<slang::IBlob> Link(slang::ISession& session, slang::IModule& module, | |
| 91 | const std::string& name, const std::string& entry) { | ||
| 92 | 105 | Slang::ComPtr<slang::IEntryPoint> found; | |
| 93 | 105 | if (SLANG_FAILED(module.findEntryPointByName(entry.c_str(), found.writeRef()))) { | |
| 94 | 3 | throw std::invalid_argument("Shader module " + name + " has no entry point " + entry); | |
| 95 | } | ||
| 96 | 104 | std::array<slang::IComponentType*, 2> parts{&module, found}; | |
| 97 | 104 | Slang::ComPtr<slang::IComponentType> program; | |
| 98 | 104 | Slang::ComPtr<slang::IComponentType> linked; | |
| 99 | 104 | Slang::ComPtr<slang::IBlob> code; | |
| 100 | 104 | Slang::ComPtr<slang::IBlob> diagnostics; | |
| 101 | 104 | const bool failed = | |
| 102 | 104 | SLANG_FAILED(session.createCompositeComponentType( | |
| 103 | 104 | parts.data(), parts.size(), program.writeRef(), diagnostics.writeRef())) || | |
| 104 | 208 | SLANG_FAILED(program->link(linked.writeRef(), diagnostics.writeRef())) || | |
| 105 | 104 | SLANG_FAILED(linked->getEntryPointCode(0, 0, code.writeRef(), diagnostics.writeRef())); | |
| 106 | 104 | if (failed) { | |
| 107 | ✗ | throw std::invalid_argument("Entry point " + entry + " of " + name + | |
| 108 | ✗ | " failed to compile:\n" + Text(diagnostics)); | |
| 109 | } | ||
| 110 | 104 | return code; | |
| 111 | 416 | } | |
| 112 | }; | ||
| 113 | |||
| 114 | 48 | ShaderCompiler::ShaderCompiler() : session_(std::make_unique<Session>()) {} | |
| 115 | |||
| 116 | 48 | ShaderCompiler::~ShaderCompiler() = default; | |
| 117 | |||
| 118 | 103 | std::vector<std::uint32_t> ShaderCompiler::Spirv(std::span<const ShaderModule> modules, | |
| 119 | std::string_view entryPoint) const { | ||
| 120 | 103 | ERNIE_ZONE("Compile Slang to SPIR-V"); | |
| 121 | 103 | const Slang::ComPtr<slang::IBlob> code = | |
| 122 | 103 | session_->Compile(modules, entryPoint, SLANG_SPIRV, "spirv_1_6"); | |
| 123 | 101 | constexpr std::size_t kWord = sizeof(std::uint32_t); | |
| 124 | 101 | if (code->getBufferSize() % kWord != 0) { | |
| 125 | ✗ | throw std::runtime_error("Slang's SPIR-V is not whole 32-bit words"); | |
| 126 | } | ||
| 127 | 101 | std::vector<std::uint32_t> words(code->getBufferSize() / kWord); | |
| 128 | 101 | std::memcpy(words.data(), code->getBufferPointer(), code->getBufferSize()); | |
| 129 | 202 | return words; | |
| 130 | 103 | } | |
| 131 | |||
| 132 | 3 | std::string ShaderCompiler::Wgsl(std::span<const ShaderModule> modules, | |
| 133 | std::string_view entryPoint) const { | ||
| 134 | 3 | ERNIE_ZONE("Compile Slang to WGSL"); | |
| 135 | 6 | return Text(session_->Compile(modules, entryPoint, SLANG_WGSL, "wgsl").get()); | |
| 136 | 3 | } | |
| 137 | |||
| 138 | } // namespace ernie | ||
| 139 |