Shader Storage buffer object syntax error caused by Apple Silicon M1 compatibility with GLSL version

Hello everyone,

I am building OpenGL engine with Apple Silicon M1 chipset.
So far I found that I can use upto GLSL 410, and some of GLSL statements from 430 are not working for me.

PBRClusteredShading.frag

#version 410 core
// Naming scheme clarification
// mS = model Space
// vS = view Space
// wS = world Space
// tS = tangent Space

out vec4 FragColor;

in VS_OUT {
    vec3 fragPos_wS;
    vec2 texCoords;
    vec4 fragPos_lS;
    vec3 T;
    vec3 B;
    vec3 N;
    mat3 TBN;
} fs_in;

// Dir light uniform
struct DirLight {
    vec3 direction;
    vec3 color;
};
uniform DirLight dirLight;

// PBR Textures to sample from
uniform sampler2D albedoMap;
uniform sampler2D emissiveMap;
uniform sampler2D normalsMap;
uniform sampler2D lightMap;
uniform sampler2D metalRoughMap;
uniform sampler2D shadowMap;

// IBL textures to sample, all pre-computed
// Really these would be mostly the same for all objects, so why not make this be binded directly?
uniform samplerCube irradianceMap;
uniform samplerCube prefilterMap;
uniform sampler2D brdfLUT;

uniform vec3 cameraPos_wS;

// lights there will be in a given scene
#define SHADOW_CASTING_POINT_LIGHTS 4
#define M_PI 3.1415926535897932384626433832795

// Cluster shading structs and buffers
struct PointLight {
    vec4 position;
    vec4 color;
    bool enabled;
    float intensity;
    float range;
};

struct LightGrid {
    uint offset;
    uint count;
};

layout (std430, binding = 2) buffer screenToView {
    mat4 inverseProjection;
    uvec4 tileSizes;
    uvec2 screenDimensions;
    float scale;
    float bias;
};

layout (std430, binding = 3) buffer lightSSBO {
    PointLight pointLight[];
};

layout (std430, binding = 4) buffer lightIndexSSBO {
    uint globalLightIndexList[];
};

layout (std430, binding = 5) buffer lightGridSSBO {
    LightGrid lightGrid[];
};

vec3 sampleOffsetDirections[20] = vec3[] (
   vec3( 1,  1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1,  1,  1), 
   vec3( 1,  1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1,  1, -1),
   vec3( 1,  1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1,  1,  0),
   vec3( 1,  0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1,  0, -1),
   vec3( 0,  1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0,  1, -1)
);

vec3 colors[8] = vec3[] (
   vec3(0, 0, 0),    vec3( 0,  0,  1), vec3( 0, 1, 0),  vec3(0, 1,  1),
   vec3(1,  0,  0),  vec3( 1,  0,  1), vec3( 1, 1, 0),  vec3(1, 1, 1)
);

uniform samplerCube depthMaps[SHADOW_CASTING_POINT_LIGHTS];
uniform float far_plane;
uniform float zFar;
uniform float zNear;

uniform bool normalMapped;
uniform bool aoMapped;
uniform bool IBL;
uniform bool slices;

// Function prototypes
vec3 calcDirLight(DirLight light, vec3 normal, vec3 viewDir, vec3 albedo, float rough, float metal, float shadow, vec3 F0);
float calcDirShadow(vec4 fragPosLightSpace);
vec3 calcPointLight(uint index, vec3 normal, vec3 fragPos, vec3 viewDir, vec3 albedo, float rough, float metal, vec3 F0,  float viewDistance);
float calcPointLightShadows(samplerCube depthMap, vec3 fragPos, float viewDistance);
float linearDepth(float depthSample);

// PBR Functions
vec3 fresnelSchlick(float cosTheta, vec3 F0);
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness);
float distributionGGX(vec3 N, vec3 H, float rough);
float geometrySchlickGGX(float nDotV, float rough);
float geometrySmith(float nDotV, float nDotL, float rough);

void main() {
    // Texture Reads
    vec4 color      =  texture(albedoMap, fs_in.texCoords).rgba;
    vec3 emissive   =  texture(emissiveMap, fs_in.texCoords).rgb;
    float ao        =  texture(lightMap, fs_in.texCoords).r;
    vec2 metalRough =  texture(metalRoughMap, fs_in.texCoords).bg;
    float metallic  =  metalRough.x;
    float roughness =  metalRough.y;

    vec3 albedo = color.rgb;
    float alpha = color .a;
    // TODO: This kills perf, look for alternatives?
    if (alpha < 0.5) {
        discard;
    }
    
    // Normal mapping
    vec3 norm = vec3(0.0);
    if (normalMapped) {
        vec3 normal = normalize(2.0 * texture(normalsMap, fs_in.texCoords).rgb - 1.0);
        mat3 TBN  = mat3(fs_in.T, fs_in.B, fs_in.N);
        norm = normalize(TBN * normal ); //going -1 to 1
    }

    else {
        // Default to using the vertex normal if no normal map is used
        norm = normalize(fs_in.N);
    }

    // Components common to all light types
    vec3 viewDir = normalize(cameraPos_wS - fs_in.fragPos_wS);
    vec3 R = reflect(-viewDir, norm);

    // Correcting zero incidence reflection
    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    // Locating which cluster you are a part of
    uint zTile     = uint(max(log2(linearDepth(gl_FragCoord.z)) * scale + bias, 0.0));
    uvec3 tiles    = uvec3( uvec2( gl_FragCoord.xy / tileSizes[3] ), zTile);
    uint tileIndex = tiles.x +
                     tileSizes.x * tiles.y +
                     (tileSizes.x * tileSizes.y) * tiles.z;  

    // Solving outgoing reflectance of fragment
    vec3 radianceOut = vec3(0.0);

    // Shadow calcs
    float shadow = calcDirShadow(fs_in.fragPos_lS);
    float viewDistance = length(cameraPos_wS - fs_in.fragPos_wS);

    // Directional light 
    radianceOut = calcDirLight(dirLight, norm, viewDir, albedo, roughness, metallic, shadow, F0) ;

    // Point lights
    uint lightCount       = lightGrid[tileIndex].count;
    uint lightIndexOffset = lightGrid[tileIndex].offset;

    // Reading from the global light list and calculating the radiance contribution of each light.
    for (uint i = 0; i < lightCount; i++) {
        uint lightVectorIndex = globalLightIndexList[lightIndexOffset + i];
        radianceOut += calcPointLight(lightVectorIndex, norm, fs_in.fragPos_wS, viewDir, albedo, roughness, metallic, F0, viewDistance);
    }

    // Treating the ambient light term as the incoming indirect light affecting the fragment
    // We have two options, if IBL is not enabled for hte given object, we use a flat ambient term
    // which generally looks terrible but it's an okay fallback
    // If IBL is enabled it will use an environment map to do a very rough incoming light approximation from it
    vec3 ambient = vec3(0.025) * albedo;
    if (IBL) {
        vec3  kS = fresnelSchlickRoughness(max(dot(norm, viewDir), 0.0), F0, roughness);
        vec3  kD = 1.0 - kS;
        kD *= 1.0 - metallic;
        vec3 irradiance = texture(irradianceMap, norm).rgb;
        vec3 diffuse    = irradiance * albedo;

        const float MAX_REFLECTION_LOD = 4.0;
        vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
        vec2 envBRDF = texture(brdfLUT, vec2(max(dot(norm, viewDir), 0.0), roughness)).rg;
        vec3 specular = prefilteredColor * (kS * envBRDF.x + envBRDF.y);
        ambient = (kD * diffuse + specular);
    }

    if (aoMapped) {
        ambient *= ao;
    }

    radianceOut += ambient;

    // Adding any emissive if there is an assigned map
    radianceOut += emissive;

    if (slices) {
        FragColor = vec4(colors[uint(mod(zTile, 8.0))], 1.0);
    }

    else {
        FragColor = vec4(radianceOut, 1.0);
    }
}

vec3 calcDirLight(DirLight light, vec3 normal, vec3 viewDir, vec3 albedo, float rough, float metal, float shadow, vec3 F0) {
    // Variables common to BRDFs
    vec3 lightDir = normalize(-light.direction);
    vec3 halfway  = normalize(lightDir + viewDir);
    float nDotV = max(dot(normal, viewDir), 0.0);
    float nDotL = max(dot(normal, lightDir), 0.0);
    vec3 radianceIn = dirLight.color;

    // Cook-Torrance BRDF
    float NDF = distributionGGX(normal, halfway, rough);
    float G   = geometrySmith(nDotV, nDotL, rough);
    vec3  F   = fresnelSchlick(max(dot(halfway,viewDir), 0.0), F0);

    // Finding specular and diffuse component
    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metal;

    vec3 numerator = NDF * G * F;
    float denominator = 4.0 * nDotV * nDotL;
    vec3 specular = numerator / max (denominator, 0.0001);

    vec3 radiance = (kD * (albedo / M_PI) + specular ) * radianceIn * nDotL;
    radiance *= (1.0 - shadow);

    return radiance;
}

// Sample offsets for the pcf are the same for both dir and point shadows
float calcDirShadow(vec4 fragPosLightSpace) {
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    projCoords = projCoords * 0.5 + 0.5;
    float bias = 0.0;
    int   samples = 9;
    float shadow = 0.0;

    vec2 texelSize = 1.0 / textureSize(shadowMap, 0);

    for (int i = 0; i < samples; ++i) {
        float pcfDepth = texture(shadowMap, projCoords.xy + sampleOffsetDirections[i].xy * texelSize).r;
        shadow += projCoords.z - bias > pcfDepth ? 0.111111 : 0.0;
    }

    return shadow;
}

vec3 calcPointLight(uint index, vec3 normal, vec3 fragPos,
                    vec3 viewDir, vec3 albedo, float rough,
                    float metal, vec3 F0,  float viewDistance) {
    // Point light basics
    vec3 position = pointLight[index].position.xyz;
    vec3 color    = 100.0 * pointLight[index].color.rgb;
    float radius  = pointLight[index].range;

    // Stuff common to the BRDF subfunctions 
    vec3 lightDir = normalize(position - fragPos);
    vec3 halfway  = normalize(lightDir + viewDir);
    float nDotV = max(dot(normal, viewDir), 0.0);
    float nDotL = max(dot(normal, lightDir), 0.0);

    // Attenuation calculation that is applied to all
    float distance    = length(position - fragPos);
    float attenuation = pow(clamp(1 - pow((distance / radius), 4.0), 0.0, 1.0), 2.0)/(1.0  + (distance * distance) );
    vec3 radianceIn   = color * attenuation;

    // Cook-Torrance BRDF
    float NDF = distributionGGX(normal, halfway, rough);
    float G   = geometrySmith(nDotV, nDotL, rough);
    vec3  F   = fresnelSchlick(max(dot(halfway,viewDir), 0.0), F0);

    // Finding specular and diffuse component
    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metal;

    vec3 numerator = NDF * G * F;
    float denominator = 4.0 * nDotV * nDotL;
    vec3 specular = numerator / max(denominator, 0.0000001);
    // vec3 specular = numerator / denominator;

    vec3 radiance = (kD * (albedo / M_PI) + specular ) * radianceIn * nDotL;

    // Shadow stuff
    vec3 fragToLight = fragPos - position;
    float shadow = calcPointLightShadows(depthMaps[index], fragToLight, viewDistance);
    
    radiance *= (1.0 - shadow);

    return radiance;
}

// Sample amount is small but this was killing perf
// This will probably be re-written as soon as the shadow mapping update comes in
float calcPointLightShadows(samplerCube depthMap, vec3 fragToLight, float viewDistance) {
    float shadow      = 0.0;
    float bias        = 0.0;
    int   samples     = 8;
    float fraction    = 1.0/float(samples);
    float diskRadius  = (1.0 + (viewDistance / far_plane)) / 25.0;
    float currentDepth = (length(fragToLight) - bias);

    for (int i = 0; i < samples; ++i) {
        float closestDepth = texture(depthMap, fragToLight + sampleOffsetDirections[i], diskRadius).r;
        closestDepth *= far_plane;

        if (currentDepth > closestDepth) {
            shadow += fraction;
        }
    }

    return shadow;
}

float linearDepth(float depthSample) {
    float depthRange = 2.0 * depthSample - 1.0;
    // Near... Far... wherever you are...
    float linear = 2.0 * zNear * zFar / (zFar + zNear - depthRange * (zFar - zNear));
    return linear;
}

// PBR functions
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    float val = 1.0 - cosTheta;
    return F0 + (1.0 - F0) * (val*val*val*val*val); //Faster than pow
}

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
    float val = 1.0 - cosTheta;
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * (val*val*val*val*val); //Faster than pow
}

float distributionGGX(vec3 N, vec3 H, float rough) {
    float a  = rough * rough;
    float a2 = a * a;

    float nDotH  = max(dot(N, H), 0.0);
    float nDotH2 = nDotH * nDotH;

    float num = a2; 
    float denom = (nDotH2 * (a2 - 1.0) + 1.0);
    denom = 1 / (M_PI * denom * denom);

    return num * denom;
}

float geometrySchlickGGX(float nDotV, float rough) {
    float r = (rough + 1.0);
    float k = r*r / 8.0;

    float num = nDotV;
    float denom = 1 / (nDotV * (1.0 - k) + k);

    return num * denom;
}

float geometrySmith(float nDotV, float nDotL, float rough) {
    float ggx2  = geometrySchlickGGX(nDotV, rough);
    float ggx1  = geometrySchlickGGX(nDotL, rough);

    return ggx1 * ggx2;
}

Above throws me an syntax error under GLSL 410, is there any alternative for this?

Thanks,

I know that object buffer binding points are supported since GLSL 430, so I want to know how I can convert these to one can be used in GLSL 410

SSBOs are a GL 4.3 feature. Unless the implementation supports the appropriate extension (and I can guarantee that Apple’s implementation does not), you cannot use them at all. It’s not about “buffer binding points”; it’s about having a buffer interface block of any kind.

The shader you posted doesn’t write to the buffers, so in this specific example you don’t need a SSBO, you can use a UBO with the “layout (std140) uniform …” syntax. You will need to bind the buffers explicitly, not in the shader layout. And you also can not dynamically index into an implicitly sized array of structs in the UBO, so size them to fit the actual buffers.

On the other hand if (any of) your shaders do need to write to the buffers (with the requisite atomics/barriers), then on an Apple platform you’ll need to rewrite your engine in Metal.

On the other hand if (any of) your shaders do need to write to the buffers (with the requisite atomics/barriers), then on an Apple platform you’ll need to rewrite your engine in Metal.

…or try/contribute to MGL, which is an implementation of OpenGL4.6 on top of Metal.

Disclaimer 1: it’s a shameless ad, as I contributed to it (I’m not the principal author though)
Disclaimer 2: I do not have much time to help and contribute to it anymore
Disclaimer 3: I fixed and added stuff to Michael’s work, to the point where I successfully made my application run, but more work/contribution is needed, and welcomed.
Disclaimer 4: it’s rough around the edges, and not for beginners, and because of Disclaimer 2, you should be able to sort out by yourselves some preliminary tasks (like building it, integrating it into your application, fixing ‘obvious’ bugs would be nice too)