copenglnvidiaintelspir-v

GLSL uniform name empty when using SPIR-V on Intel HD Graphics but not on NVIDIA


Context

I am writing a Shadertoy-like application as part of a school project, and noticed that the values of my uniform variables were not updating on my (old) Thinkpad while they were working fine on my desktop PC. My application uses compiled GLSL shaders (using glslc into SPIR-V binary), which are linked into the final executable and read using the glShaderBinary function.

I have made a MWE application to showcase this effect. The application reads the fragment and vertex shader in both GLSL source format and in SPIR-V binary format. For both formats, it links the fragment and vertex shader into a single program (called shader in the source code), and lists the names of all active uniforms. It also attempts to directly get the location of the uniform named test in shader.frag.

On Intel (i5-4200U):

vendor:        Intel
renderer:      Mesa Intel(R) HD Graphics 4400 (HSW GT2)
version:       4.6 (Core Profile) Mesa 24.0.7-arch1.3
glsl version:  4.60
------------------------------
GLSL:
    active uniform count: 1
    (location = 0) = "test"
    `test` uniform location = 0
SPIR-V:
    active uniform count: 1
    (location = 0) = ""
    `test` uniform location = -1

On NVIDIA:

vendor:        NVIDIA Corporation
renderer:      NVIDIA GeForce GTX 1050 Ti/PCIe/SSE2
version:       4.6.0 NVIDIA 550.78
glsl version:  4.60 NVIDIA
------------------------------
GLSL:
    active uniform count: 1
    (location = 0) = "test"
    `test` uniform location = 0
SPIR-V:
    active uniform count: 1
    (location = 0) = "test"
    `test` uniform location = 0

Sources

The sources are also available as a git repository:

git clone --branch bug2 https://git.pipeframe.xyz/school/project-iprj

main.c

#include <stdio.h>
#include <stdlib.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>

const uint32_t vert_spirv[] =
#include "vert_spirv.h"
;
const uint32_t frag_spirv[] =
#include "frag_spirv.h"
;
const char vert_src[] = {
#include "vert_src.h"
, 0x00 };
const char frag_src[] = {
#include "frag_src.h"
, 0x00 };

GLuint load_shader_src(GLenum type, const char* src) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &src, NULL);
    glCompileShader(shader);
    return shader;
}
GLuint load_shader_spirv(GLenum type, const char* src, size_t size) {
    GLuint shader = glCreateShader(type);
    glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, src, size);
    glSpecializeShader(shader, "main", 0, NULL, NULL);
    return shader;
}

GLuint link_shaders(GLuint vert, GLuint frag) {
    GLuint shader = glCreateProgram();
    glAttachShader(shader, vert);
    glAttachShader(shader, frag);
    glLinkProgram(shader);
    glUseProgram(shader); // <- use the program before getting uniform name
    glDeleteShader(vert);
    glDeleteShader(frag);
    return shader;
}

void test(const char* label, GLuint shader) {
    printf("%s:\n", label);

    // list all ACTIVE uniforms in shader
    GLint count;
    glGetProgramiv(shader, GL_ACTIVE_UNIFORMS, &count);
    printf("\tactive uniform count: %d\n", count);
    for (unsigned i = 0; i < count; i++) {
        GLchar name[80];
        GLsizei length;
        glGetActiveUniformName(shader, i, 80, &length, name);
        printf("\t(location = %u) = \"%.*s\"\n", i, length, name);
    }

    // try directly geting uniform location
    GLint uniform_location = glGetUniformLocation(shader, "test");
    printf("\t`test` uniform location = %d\n", uniform_location);
}

int main(int argc, char** argv) {
    // initialize window
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
    GLFWwindow* window = glfwCreateWindow(800, 600, "test", NULL, NULL);
    glfwMakeContextCurrent(window);
    glewInit();

    // print driver info
    printf("vendor:        %s\n", glGetString(GL_VENDOR));
    printf("renderer:      %s\n", glGetString(GL_RENDERER));
    printf("version:       %s\n", glGetString(GL_VERSION));
    printf("glsl version:  %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));
    printf("------------------------------\n");

    // test w/ glsl source
    GLuint src_vert = load_shader_src(GL_VERTEX_SHADER, vert_src);
    GLuint src_frag = load_shader_src(GL_FRAGMENT_SHADER, frag_src);
    GLuint src_shader = link_shaders(src_vert, src_frag);
    test("GLSL", src_shader);

    // test w/ spir-v
    GLuint spirv_vert = load_shader_spirv(GL_VERTEX_SHADER, (char*) vert_spirv, sizeof(vert_spirv));
    GLuint spirv_frag = load_shader_spirv(GL_FRAGMENT_SHADER, (char*) frag_spirv, sizeof(frag_spirv));
    GLuint spirv_shader = link_shaders(spirv_vert, spirv_frag);
    test("SPIR-V", spirv_shader);

    return 0;
}

makefile (GNU make)

LDFLAGS += -lglfw
LDFLAGS += -lOpenGL
LDFLAGS += -lGLEW

GLFLAGS += --target-env=opengl
GLFLAGS += -fauto-map-locations

main: main.c

main.c: vert_spirv.h
main.c: frag_spirv.h
main.c: vert_src.h
main.c: frag_src.h

frag_spirv.h: shader.frag
    glslc $(GLFLAGS) -mfmt=c -o $@ $<
vert_spirv.h: shader.vert
    glslc $(GLFLAGS) -mfmt=c -o $@ $<
frag_src.h: shader.frag
    xxd -i < $< > $@
vert_src.h: shader.vert
    xxd -i < $< > $@

shader.vert

#version 460 core
layout (location = 0) in vec3 vert;

void main() {
    gl_Position = vec4(vert.xyz, 0.001);
}

shader.frag

#version 460 core
uniform float test;
layout(location = 0) out vec4 color;

void main() {
    vec2 uv = gl_FragCoord.xy / ivec2(800, 600);
    color = vec4(uv.xy, test, 1.0);
}

Problem

The uniform name becomes empty when running the SPIR-V shaders on Intel HD Graphics. The test uniform is still active as shown in the above output. When I hard-code the location of the test uniform instead of using glGetUniformLocation, updating its value works fine on both Intel and NVIDIA, but I would like to avoid this approach as I have no control over the location.

I have tried the following, but they all did not have any effect on the output of the MWE:

I would like to keep using the SPIR-V compiler, as it allows me to use the C preprocessor and catch GLSL compiler warnings during compile-time instead of during runtime.


Solution

  • The behavior of this program is according to the specifications (emphasis mine):

    Because SPIR-V is an intermediate language, things like names are unnecessary. As such, while SPIR-V does permit you to assign a name to a particular construct, it does not require you to do so. Therefore, any OpenGL introspection query that involves the name of a SPIR-V variable or other construct may not produce reasonable results.

    As the program output indicates, the Intel implementation does detect the (active) uniform variable. Only its name cannot reliably be queried. User-defined variables in SPIR-V can only be interfaced with using locations:

    Input/output interface matching for user-defined variables in SPIR-V works by matching explicit Locations. As such, all variables that are used for input/output interfaces must have a location assigned.