我正在尝试使用 glslang 将 glsl 着色器代码编译为 SPIR-V 二进制文件。 glslang 项目可以在这里找到:
https://github.com/KhronosGroup/glslang
通过在命令行中手动运行 glslangValidator.exe 可以很好地工作。 但我想使用 C++ 接口。
我已经按照 github 页面上的描述构建了项目,现在我正在努力解决如何实际使用该界面的问题。
我宁愿不在我的解决方案中实际包含任何项目(我正在使用 Visual Studio),而是链接使用它所需的 .lib 和标头。 我只是找不到我需要链接哪些。 github页面只提到了ShaderLang.h和StandAlone.cpp,这是不够的。
有人可以解释如何设置一个可以使用 glslang 的项目吗?我只需要它来将 glsl 着色器代码编译为 SPIR-V 二进制文件(带有有关着色器编译的调试信息)。 我想对于已经做过或有更多经验的人来说,这将是一个非常简单的问题。
您需要使用几个库。一个可供查看的消费者示例是 LunarGLASS:https://github.com/LunarG/LunarGLASS。在那里,您可以看到该文件:
https://github.com/LunarG/LunarGLASS/blob/master/CMakeLists.txt
哪个里面包含这个库:
set(GLSLANGLIBS
glslang
HLSL
OSDependent
OGLCompiler
SPIRV)
glslang 的自述文件包含一些重要信息。此外,在 glslang 中,glslangValidator 工具(基本上是 StandAlone.cpp)展示了如何使用库的 API。您还可以在 LunarGLASS 项目中查看 Frontends/glslang 目录以进行类似的用途。
Shaderc 项目位于 https://github.com/google/shaderc 提供了一个易于使用的 C++ API,它将 Glslang 的编译器包装到 SPIR-V。
有关示例用法,请参阅 https://github.com/google/shaderc/blob/master/examples/online-compile/main.cc
您可以使用
glslang
C 接口 (https://github.com/KhronosGroup/glslang/pull/2038) 将着色器源代码编译为 SPIR-V,如下所示。
const char* shaderCodeVertex = ...;
const glslang_input_t input =
{
.language = GLSLANG_SOURCE_GLSL,
.stage = GLSLANG_STAGE_VERTEX,
.client = GLSLANG_CLIENT_VULKAN,
.client_version = GLSLANG_TARGET_VULKAN_1_1,
.target_language = GLSLANG_TARGET_SPV,
.target_language_version = GLSLANG_TARGET_SPV_1_3,
.code = shaderCodeVertex,
.default_version = 100,
.default_profile = GLSLANG_NO_PROFILE,
.force_default_version_and_profile = false,
.forward_compatible = false,
.messages = GLSLANG_MSG_DEFAULT_BIT,
};
glslang_initialize_process();
glslang_shader_t* shader = glslang_shader_create( &input );
if ( !glslang_shader_preprocess(shader, &input) )
{
// use glslang_shader_get_info_log() and glslang_shader_get_info_debug_log()
}
if ( !glslang_shader_parse(shader, &input) )
{
// use glslang_shader_get_info_log() and glslang_shader_get_info_debug_log()
}
glslang_program_t* program = glslang_program_create();
glslang_program_add_shader( program, shader );
if (!glslang_program_link(program, GLSLANG_MSG_SPV_RULES_BIT | GLSLANG_MSG_VULKAN_RULES_BIT))
{
// use glslang_program_get_info_log() and glslang_program_get_info_debug_log();
}
glslang_program_SPIRV_generate( program, input.stage );
if ( glslang_program_SPIRV_get_messages(program) )
{
printf("%s", glslang_program_SPIRV_get_messages(program));
}
glslang_shader_delete( shader );
编译后的 SPIR-V blob 可以通过以下方式与 Vulkan 一起使用。
const VkShaderModuleCreateInfo ci =
{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = glslang_program_SPIRV_get_size(program) * sizeof(unsigned int),
.pCode = glslang_program_SPIRV_get_ptr(program)
};
VkResult result = vkCreateShaderModule(device, &ci, nullptr, ...);
glslang_program_delete( program );
以下内容对我有用。 CMakeLists.txt:
...
find_package(glslang CONFIG REQUIRED)
find_package(Threads REQUIRED)
...
target_link_libraries(target_name
PUBLIC
glslang::glslang
glslang::SPIRV
glslang::glslang-default-resource-limits)
...
glslang::glslang-default-resource-limits
是 glslang_default_resource()
功能所必需的。
main()
功能:
#include <glslang/Include/glslang_c_interface.h>
int main() {
glslang_initialize_process();
...
glslang_finalize_process();
return 0;
}
别忘了将
resource
传递给 glslang_input_t
:
#include <glslang/Include/glslang_c_interface.h>
#include <glslang/Public/resource_limits_c.h>
...
const glslang_input_t input =
{
.language = GLSLANG_SOURCE_GLSL,
.stage = GLSLANG_STAGE_COMPUTE,
.client = GLSLANG_CLIENT_VULKAN,
.client_version = GLSLANG_TARGET_VULKAN_1_1,
.target_language = GLSLANG_TARGET_SPV,
.target_language_version = GLSLANG_TARGET_SPV_1_3,
.code = shaderCode,
.default_version = 460,
.default_profile = GLSLANG_CORE_PROFILE,
.force_default_version_and_profile = 0,
.forward_compatible = 0,
.messages = GLSLANG_MSG_DEFAULT_BIT,
.resource = glslang_default_resource() // Load defaults or create resource manualy!
};
...
着色器加载器:
const char* shaderCode = ...;
glslang_shader_t* shader = glslang_shader_create(&input);
if (!glslang_shader_preprocess(shader, &input)) { /* errors and logs */ }
if (!glslang_shader_parse(shader, &input)) { /* errors and logs */ }
glslang_program_t* program = glslang_program_create();
glslang_program_add_shader(program, shader);
if (!glslang_program_link(program, GLSLANG_MSG_SPV_RULES_BIT | GLSLANG_MSG_VULKAN_RULES_BIT))
{ /* errors and logs */ }
glslang_program_SPIRV_generate(program, input.stage);
if (glslang_program_SPIRV_get_messages(program))
{ /* errors and logs */ }
const VkShaderModuleCreateInfo info =
{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = glslang_program_SPIRV_get_size(program) * sizeof(unsigned int),
.pCode = glslang_program_SPIRV_get_ptr(program)
};
VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &info, NULL, &shaderModule) != VK_SUCCESS) {
/* errors and logs */
}
...
glslang_shader_delete(shader);
glslang_program_delete(program);
虽然是一个有点老的问题,但我最近在使用 C++ 接口方面遇到了困难,所以我决定在这里发布我的发现。
这可能是最棘手的部分。这是我们在编写 C 或 C++ 程序时不会考虑的事情,但 GLSL 允许通过
#include
预处理指令包含其他着色器文件,并且它的行为不是为我们定义的。
在文件
glslang/Public/ShaderLang.h
中我们定义了一个抽象类,总结一下:
class Includer {
public:
struct IncludeResult {
// [...]
};
virtual IncludeResult* includeSystem(const char* /*headerName*/,
const char* /*includerName*/,
size_t /*inclusionDepth*/) { return nullptr; }
virtual IncludeResult* includeLocal(const char* /*headerName*/,
const char* /*includerName*/,
size_t /*inclusionDepth*/) { return nullptr; }
virtual void releaseInclude(IncludeResult*) = 0;
virtual ~Includer() {}
};
它的用法只是非常模糊地描述,但从我所收集的信息(通过阅读代码)我们应该创建一个继承自
Includer
的类,并实现上面显示的方法。从本质上讲,这些方法在本地文件系统中搜索匹配 headerName
的 GLSL 文件,读取其内容,并在新的 Includer::IncludeResult
结构中返回其内容。
更麻烦的是,此标头查找本身可能会导致递归查询!
这个实现大致是从我自己的代码复制/粘贴的(不幸的是我无法开源atm)。理解它的工作原理应该相当容易:
// includes
#include <glslang/Public/ShaderLang.h>
#include <map>
#include <vector>
class GlslShaderIncluder : public glslang::TShader::Includer
{
public:
// explicit GlslShaderIncluder(fileio::Directory* shaderdir)
// : mShaderdir(shaderdir) {}
// Note "local" vs. "system" is not an "either/or": "local" is an
// extra thing to do over "system". Both might get called, as per
// the C++ specification.
//
// For the "system" or <>-style includes; search the "system" paths.
virtual IncludeResult* includeSystem(
const char* headerName, const char* includerName, size_t inclusionDepth) override;
// For the "local"-only aspect of a "" include. Should not search in the
// "system" paths, because on returning a failure, the parser will
// call includeSystem() to look in the "system" locations.
virtual IncludeResult* includeLocal(
const char* headerName, const char* includerName, size_t inclusionDepth) override;
virtual void releaseInclude(IncludeResult*) override;
private:
static inline const std::string sEmpty = "";
static inline IncludeResult smFailResult =
IncludeResult(sEmpty, "Header does not exist!", 0, nullptr);
// const fileio::Directory* mShaderdir {nullptr};
std::map<std::string, IncludeResult*> mIncludes;
std::map<std::string, std::vector<char>> mSources;
};
主要由于时间限制,实施工作尚未完成。不支持系统包含(缺少用例),但这应该类似于处理本地包含的方式。该代码还依赖于自定义文件 I/O 库,因此这些部分被注释掉(它也可以使用
std::ifstream
或 fopen
,fileio
命名空间在下面使用 std::filesystem
):
IncludeResult* GlslShaderIncluder::includeSystem(
const char* headerName, const char* includerName, size_t inclusionDepth)
{
// TODO: This should be used if a shader file says "#include <source>",
// in which case it includes a "system" file instead of a local file.
log_error("GlslShaderIncluder::includeSystem() is not implemented!");
log_error("includeSystem({}, {}, {})", headerName, includerName, inclusionDepth);
return nullptr;
}
IncludeResult* GlslShaderIncluder::includeLocal(
const char* headerName, const char* includerName, size_t inclusionDepth)
{
log_debug("includeLocal({}, {}, {})", headerName, includerName, inclusionDepth);
// std::string resolvedHeaderName =
// fileio::directory_get_absolute_path(mShaderdir, headerName);
if (auto it = mIncludes.find(resolvedHeaderName); it != mIncludes.end())
{
// `headerName' was already present, so return that, and probably log about it
return it->second;
}
// if (!fileio::file_exists(mShaderdir, headerName))
// {
// log_error("#Included GLSL shader file \"{}\" does not exist!", resolvedHeaderName);
// return &smFailResult;
// }
// mSources[resolvedHeaderName] = {}; // insert an empty vector!
// fileio::File* file = fileio::file_open(
// mShaderdir, headerName, fileio::FileModeFlag::read);
// if (file == nullptr)
// {
// log_error("Failed to open #included GLSL shader file: {}", resolvedHeaderName);
// return &smFailResult;
// }
// if (!fileio::file_read_into_buffer(file, mSources[resolvedHeaderName]))
// {
// log_error("Failed to read #included GLSL shader file: {}", resolvedHeaderName);
// fileio::file_close(file);
// return &smFailResult;
// }
IncludeResult* result = new IncludeResult(
resolvedHeaderName, mSources[resolvedHeaderName].data(),
mSources[resolvedHeaderName].size(), nullptr);
auto [it, b] = mIncludes.emplace(std::make_pair(resolvedHeaderName, result));
if (!b)
{
log_error("Failed to insert IncludeResult into std::map!");
return &smFailResult;
}
return it->second;
}
void GlslShaderIncluder::releaseInclude(IncludeResult* result)
{
log_debug("releaseInclude(result->headerName: {})", result->headerName);
if (auto it = mSources.find(result->headerName); it != mSources.end())
{
mSources.erase(it);
}
if (auto it = mIncludes.find(result->headerName); it != mIncludes.end())
{
// EDIT: I have forgotten to use "delete" here on the IncludeResult, but should probably be done!
mIncludes.erase(it);
}
}
定义自定义包含器后,我们可以继续将其注入到
glslang
加载器(这也是一个对象)中:
std::vector<char> buffer; // contains the shader file
glslang::TShader shader(EShLangVertex); // example, use this code for each separate shader file in the shader program
const char* sources[1] = { buffer.data() };
shader.setStrings(sources, 1);
// Use appropriate Vulkan version
glslang::EShTargetClientVersion targetApiVersion = glslang::EShTargetVulkan_1_3;
shader.setEnvClient(glslang::EShClientVulkan, targetApiVersion);
glslang::EShTargetLanguageVersion spirvVersion = glslang::EShTargetSpv_1_3;
shader.setEnvTarget(glslang::EshTargetSpv, spirvVersion);
shader.setEntryPoint("main"); // We can specify a different entry point
// The resource is an entire discussion in and by itself, here just use default.
TBuiltInResource* resources = GetDefaultResources();
// int defaultVersion = 110, // use 100 for ES environment, overridden by #version in shader
const int defaultVersion = 450;
const bool forwardCompatible = false;
const EShMessages messageFlags = (EShMessages)(EShMsgSpvRules | EShMsgVulkanRules);
EProfile defaultProfile = ENoProfile; // NOTE: Only for desktop, before profiles showed up!
// NOTE: Here a custom file I/O library is used, your implementation may be different.
fileio::Directory* shaderdir = ...
GlslShaderIncluder includer(shaderdir);
std::string preprocessedStr;
if (!shader.preprocess(
resources, defaultVersion, defaultProfile, false, forwardCompatible, messageFlags, &preprocessedStr, includer))
{
log_error("Failed to preprocess shader: {}", shader.getInfoLog());
// FAIL
}
const char* preprocessedSources[1] = { preprocessedStr.c_str() };
shader.setStrings(preprocessedSources, 1);
if (!shader.parse(resources, defaultVersion, defaultProfile, false,
forwardCompatible, messageFlags, includer))
{
vtek_log_error("Failed to parse shader: {}", shader.getInfoLog());
// FAIL
}
glslang::TProgram program;
program.addShader(&shader);
if (!program.link(messageFlags))
{
vtek_log_error("Failed to link shader: {}", program.getInfoLog());
// FAIL
}
// Convert the intermediate generated by glslang to Spir-V
glslang::TIntermediate& intermediateRef = *(program.getIntermediate(lang));
std::vector<uint32_t> spirv;
glslang::SpvOptions options{};
options.validate = true;
// TODO: We can also provide a logger to glslang.
// glslang::spv::SpvBuildLogger logger;
// glslang::GlslangToSpv(intermediateRef, spirv, &logger, &options);
glslang::GlslangToSpv(intermediateRef, spirv, &options);
最后,我们有一个数组
std::vector<uint32_t> spirv
,其中包含 SPIR-V 字节码。这可用于创建 Vulkan 着色器模块。这里,我必须注意,codeSize
应该乘以4,以确保我们向GPU发送正确的字节数:
VkShaderModule module;
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = spirvCode.size() * sizeof(uint32_t);
createInfo.pCode = spirvCode.data();
VkResult result = vkCreateShaderModule(dev, &createInfo, nullptr, &module);
if (result != VK_SUCCESS)
{
log_error("Failed to create {} shader module!", type);
// FAIL
}
大致来说就是这样。该系统处理递归本地包含,并进行一些错误处理。我希望它能帮助下一个人。