7

I am trying to use glslang to compile glsl shader code to SPIR-V binaries. The glslang project can be found here:

https://github.com/KhronosGroup/glslang

It works well via the glslangValidator.exe manually in the command line. But i would like to use the c++ interface.

I have build the project as described on the github page, and now I am struggling with how to actually use the interface.

I would rather not actually include any projects in my solution (I am using Visual Studio), but link the .lib's and headers needed to use it. I just cannot find out which ones i need to link. The github page only mentions ShaderLang.h and StandAlone.cpp, which is not enough.

Can someone explain how to setup a project where you can use glslang? I need it only for compiling glsl shader code to SPIR-V binaries (with debugging info about the shader compilation). I Suppose this would be a very easy question for someone who has already done it, or has more experience.

Sergey K.
  • 24,894
  • 13
  • 106
  • 174
Aedoro
  • 609
  • 1
  • 6
  • 21

5 Answers5

5

There are several libs you need to use. An example consumer to look at is LunarGLASS: https://github.com/LunarG/LunarGLASS. There, you can see the file:

https://github.com/LunarG/LunarGLASS/blob/master/CMakeLists.txt

Which inside contains this for the libraries:

set(GLSLANGLIBS
    glslang
    HLSL
    OSDependent
    OGLCompiler
    SPIRV)

The readme for glslang contains some important information. In addition, within glslang, the glslangValidator tool (basically StandAlone.cpp) shows how to use the API for the libraries. You can also see the Frontends/glslang directory in the LunarGLASS project for a similar use.

vallentin
  • 23,478
  • 6
  • 59
  • 81
johnkslang
  • 66
  • 2
  • 1
    Thank you for the help, would something like this be a correct setup? https://i.gyazo.com/3fbe9a06d1bf536222f2204d317b8321.png. (It is more clearly visible for you if i use pragma libs and full includes like this. Obviously the project settings need to be edited with include directories and lib) – Aedoro Jul 09 '16 at 11:43
  • Looks okay; obviously depends on whether your environment meshes with that. If it links and works, I think you're good. – johnkslang Jul 11 '16 at 04:41
5

The Shaderc project at https://github.com/google/shaderc provides an easy-to-use C++ API that wraps around Glslang's compiler to SPIR-V.

For example usage, see https://github.com/google/shaderc/blob/master/examples/online-compile/main.cc

David Neto
  • 126
  • 3
  • 5
    This answer relies completely on the content of external links. Should they ever become invalid, your answer would be useless. So please [edit] your answer and add at least a summary of what can be found there. Thank you! – Fabio says Reinstate Monica Aug 30 '16 at 20:00
5

You can use the glslang C interface (https://github.com/KhronosGroup/glslang/pull/2038) to compile you shader source code into SPIR-V as follows.

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 );

The compiled SPIR-V blob can be used with Vulkan the following way.

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 );
Sergey K.
  • 24,894
  • 13
  • 106
  • 174
  • This didn't work for me at least as it crashes on glslang_input_t due resource member variable of glslang_input_t being uninitialized. – Iñigo Feb 09 '23 at 15:11
  • 1
    @Iñigo https://github.com/PacktPublishing/3D-Graphics-Rendering-Cookbook/blob/master/shared/UtilsVulkan.cpp#L157 – Sergey K. Feb 10 '23 at 21:04
1

The following works for me. 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 is needed for glslang_default_resource() function.

main() function:

#include <glslang/Include/glslang_c_interface.h>

int main() {
   glslang_initialize_process();
   ...
   glslang_finalize_process();
   return 0;
}

Don't forget to pass resource to 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!
    };
...

The shader loader:

    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);
0

How to use the C++ interface

Albeit slightly old question, I recently struggled with using the C++ interface, so I decided to post my findings here.

The includer

This is probably the most tricky part. Something that we don't think about when we write C or C++ programs, but GLSL allows the inclusion of other shader files through #include pre-processing directive, and its behaviour is not defined for us.

In the file glslang/Public/ShaderLang.h we have an abstract class defined, summarized:

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() {}
};

Its usage is only very vaguely described, but from what I can gather (by reading the code) we should create a class which inherits from Includer, and which implements the methods shown above. In essense, these methods search the local filesystem for a GLSL file matching headerName, reads its contents, and returns its content in a new Includer::IncludeResult struct.

And to make it even more problematic, this header lookup might itself result in a recursive query!

Custom includer

This implementation is roughly copy/pasted from my own code (which unfortunately I cannot open-source atm.). It should be reasonably easy to understand how it works:

// 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;
};

The implementation was, mostly because of time limits, unfinished. System includes are not supported (missing use case), but that should be similar to how local includes are handled. The code also depends on a custom file I/O library, so those parts are commented out (it would also work with using std::ifstream or fopen, the fileio namespaces uses std::filesystem underneath):

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);
    }
}

Using the includer

After defining a custom includer, we can procede to inject it into the glslang loader (which is also an object):

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);

At the end, we have an array std::vector<uint32_t> spirv which contains the SPIR-V bytecode. This may be used to create a Vulkan shader module. Here, I must note that codeSize should be multiplied with 4 to ensure that we send the correct number of bytes to the 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
}

Roughly speaking, that's it. This system handles recursive local includes, with some error handling. I hope it helps the next person.

alexpanter
  • 1,222
  • 10
  • 25