From 53e308bf1d4d665489f10070bb415fb2979bf4b1 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 21:28:05 +0300 Subject: [PATCH 1/5] build: vendor spirv-cross and generate gl and hlsl shader variants SPIRV-Cross is pinned to vulkan-sdk-1.4.363.0 and built as a host tool. sleak_cross_compile_shader() turns a shader's SPIR-V into GL GLSL and SM 5.0 HLSL in the build tree, staged next to the .spv. Only the shaders listed in ENGINE_CROSS_SHADERS are generated. --- .gitmodules | 3 ++ CMakeLists.txt | 101 +++++++++++++++++++++++++++++++++++++++++--- vendors/SPIRV-Cross | 1 + 3 files changed, 100 insertions(+), 5 deletions(-) create mode 160000 vendors/SPIRV-Cross diff --git a/.gitmodules b/.gitmodules index cf8d4a4d..c40b7170 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,6 @@ [submodule "Engine/vendors/assimp"] path = vendors/assimp url = https://github.com/assimp/assimp.git +[submodule "vendors/SPIRV-Cross"] + path = vendors/SPIRV-Cross + url = https://github.com/KhronosGroup/SPIRV-Cross diff --git a/CMakeLists.txt b/CMakeLists.txt index 5847078f..3bb99478 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -371,6 +371,96 @@ else() set(ENGINE_SPIRV_FILES ${ENGINE_COMMITTED_SPIRV}) endif() +# --- GL GLSL and HLSL generated from the SPIR-V by SPIRV-Cross --- +# Cross compiling builds can not run the vendored tool, so they need a host +# spirv-cross (Vulkan SDK) passed in or on PATH. +if(CMAKE_CROSSCOMPILING) + find_program(SLEAK_SPIRV_CROSS spirv-cross + HINTS "$ENV{VULKAN_SDK}/Bin" "$ENV{VULKAN_SDK}/bin") + if(NOT SLEAK_SPIRV_CROSS) + message(FATAL_ERROR + "Cross compiling needs a host spirv-cross to generate the GL and " + "HLSL shaders. Install the Vulkan SDK or set SLEAK_SPIRV_CROSS.") + endif() +else() + function(_engine_add_spirv_cross) + set(SPIRV_CROSS_ENABLE_TESTS OFF CACHE BOOL "" FORCE) + set(SPIRV_CROSS_SKIP_INSTALL ON CACHE BOOL "" FORCE) + set(SPIRV_CROSS_ENABLE_C_API OFF CACHE BOOL "" FORCE) + set(_out "${CMAKE_CURRENT_BINARY_DIR}/spirv-cross") + foreach(_kind RUNTIME LIBRARY ARCHIVE) + set(CMAKE_${_kind}_OUTPUT_DIRECTORY "${_out}") + foreach(_cfg ${CMAKE_CONFIGURATION_TYPES} ${CMAKE_BUILD_TYPE}) + string(TOUPPER "${_cfg}" _cfg) + set(CMAKE_${_kind}_OUTPUT_DIRECTORY_${_cfg} "${_out}") + endforeach() + endforeach() + add_subdirectory(vendors/SPIRV-Cross EXCLUDE_FROM_ALL) + endfunction() + _engine_add_spirv_cross() + set(SLEAK_SPIRV_CROSS spirv-cross) +endif() +set_property(GLOBAL PROPERTY SLEAK_SPIRV_CROSS "${SLEAK_SPIRV_CROSS}") + +# Writes .glsl (GL 4.0 with 420pack bindings, varyings matched by name) +# and .hlsl (SM 5.0) for one SPIR-V module into out_dir. +# FLIP_Y undoes a Vulkan clip space Y flip done in the vertex shader. +function(sleak_cross_compile_shader spirv out_dir outputs_var) + cmake_parse_arguments(PARSE_ARGV 3 ARG "FLIP_Y" "" "") + get_property(_tool GLOBAL PROPERTY SLEAK_SPIRV_CROSS) + get_filename_component(_name "${spirv}" NAME) + string(REGEX REPLACE "\\.spv$" "" _name "${_name}") + set(_glsl "${out_dir}/${_name}.glsl") + set(_hlsl "${out_dir}/${_name}.hlsl") + set(_flip "") + if(ARG_FLIP_Y) + set(_flip --flip-vert-y) + endif() + add_custom_command( + OUTPUT "${_glsl}" "${_hlsl}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${out_dir}" + COMMAND ${_tool} "${spirv}" --output "${_glsl}" + --version 400 --no-es --glsl-emit-push-constant-as-ubo ${_flip} + COMMAND ${_tool} "${spirv}" --output "${_hlsl}" + --hlsl --shader-model 50 + DEPENDS "${spirv}" + COMMENT "Generating GL and HLSL variants of ${_name}" + VERBATIM + ) + set(${outputs_var} ${${outputs_var}} "${_glsl}" "${_hlsl}" PARENT_SCOPE) +endfunction() + +# Authored once as Vulkan GLSL. The loaders prefer these generated variants +# and fall back to hand-written ones for every shader not listed here. +set(ENGINE_CROSS_SHADERS + debug_line.frag + ibl_brdf_lut.vert + ibl_brdf_lut.frag + ibl_irradiance.vert + ibl_irradiance.frag + ibl_prefilter.vert + ibl_prefilter.frag + lighting_pass.vert +) +set(ENGINE_CROSS_SHADERS_FLIP_Y + debug_line.vert +) +set(ENGINE_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") +set(ENGINE_GENERATED_SHADERS "") +foreach(_name ${ENGINE_CROSS_SHADERS} ${ENGINE_CROSS_SHADERS_FLIP_Y}) + if(GLSLC) + set(_spv "${ENGINE_SPIRV_DIR}/${_name}.spv") + else() + set(_spv "${ENGINE_SHADER_DIR}/${_name}.spv") + endif() + set(_flags "") + if(_name IN_LIST ENGINE_CROSS_SHADERS_FLIP_Y) + set(_flags FLIP_Y) + endif() + sleak_cross_compile_shader("${_spv}" "${ENGINE_GENERATED_DIR}" + ENGINE_GENERATED_SHADERS ${_flags}) +endforeach() + set(_stage_outputs "") if(PROJECT_IS_TOP_LEVEL) set(_stamp "${CMAKE_CURRENT_BINARY_DIR}/engine_spirv_staged.stamp") @@ -379,20 +469,21 @@ if(PROJECT_IS_TOP_LEVEL) OUTPUT "${_stamp}" COMMAND ${CMAKE_COMMAND} -E make_directory "${_dst}" COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${ENGINE_SPIRV_FILES} "${_dst}" + ${ENGINE_SPIRV_FILES} ${ENGINE_GENERATED_SHADERS} "${_dst}" COMMAND ${CMAKE_COMMAND} -E touch "${_stamp}" - DEPENDS ${ENGINE_SPIRV_FILES} - COMMENT "Staging engine SPIR-V into bin/assets/shaders" + DEPENDS ${ENGINE_SPIRV_FILES} ${ENGINE_GENERATED_SHADERS} + COMMENT "Staging engine shaders into bin/assets/shaders" VERBATIM ) list(APPEND _stage_outputs "${_stamp}") endif() add_custom_target(CompileShaders ALL - DEPENDS ${ENGINE_SPIRV_FILES} ${_stage_outputs}) + DEPENDS ${ENGINE_SPIRV_FILES} ${ENGINE_GENERATED_SHADERS} ${_stage_outputs}) add_dependencies(Engine CompileShaders) -set_property(GLOBAL PROPERTY ENGINE_SPIRV_FILES ${ENGINE_SPIRV_FILES}) +set_property(GLOBAL PROPERTY ENGINE_SPIRV_FILES + ${ENGINE_SPIRV_FILES} ${ENGINE_GENERATED_SHADERS}) set_property(GLOBAL PROPERTY ENGINE_COMMITTED_SPIRV ${ENGINE_COMMITTED_SPIRV}) function(_engine_collect_targets dir out) diff --git a/vendors/SPIRV-Cross b/vendors/SPIRV-Cross new file mode 160000 index 00000000..f11ba9f0 --- /dev/null +++ b/vendors/SPIRV-Cross @@ -0,0 +1 @@ +Subproject commit f11ba9f0b21ba8fc15153d50a2a1ae31ab1cf8f7 From cf93d72e62b46d9d27fb2f246047037ac405f01c Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 21:28:05 +0300 Subject: [PATCH 2/5] feat: prefer generated glsl in the opengl shader loader --- .../private/Graphics/OpenGL/OpenGLShader.hpp | 3 +++ src/Graphics/OpenGL/OpenGLShader.cpp | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/include/private/Graphics/OpenGL/OpenGLShader.hpp b/include/private/Graphics/OpenGL/OpenGLShader.hpp index 37537319..171ef2e5 100644 --- a/include/private/Graphics/OpenGL/OpenGLShader.hpp +++ b/include/private/Graphics/OpenGL/OpenGLShader.hpp @@ -26,6 +26,9 @@ class ENGINE_API OpenGLShader : public Shader { bool CompileShaderSource(const std::string& source, GLenum type, GLuint& shader); std::string ReadFile(const std::string& path); + /// Maps name_gl.vert/.frag to the generated name.vert.glsl/.frag.glsl + /// when that file exists, otherwise returns the path unchanged. + static std::string ResolveGenerated(const std::string& path); GLuint m_program = 0; }; diff --git a/src/Graphics/OpenGL/OpenGLShader.cpp b/src/Graphics/OpenGL/OpenGLShader.cpp index 65984ac4..fa8203c0 100644 --- a/src/Graphics/OpenGL/OpenGLShader.cpp +++ b/src/Graphics/OpenGL/OpenGLShader.cpp @@ -1,5 +1,7 @@ #include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" + #include +#include #include #include @@ -25,8 +27,10 @@ bool OpenGLShader::compile(const std::string& shaderPath) { return compile(basePath + "_gl.vert", basePath + "_gl.frag"); } -bool OpenGLShader::compile(const std::string& vertPath, - const std::string& fragPath) { +bool OpenGLShader::compile(const std::string& vertFile, + const std::string& fragFile) { + const std::string vertPath = ResolveGenerated(vertFile); + const std::string fragPath = ResolveGenerated(fragFile); std::string vertSource = ReadFile(vertPath); std::string fragSource = ReadFile(fragPath); @@ -96,6 +100,18 @@ bool OpenGLShader::CompileShaderSource(const std::string& source, return true; } +std::string OpenGLShader::ResolveGenerated(const std::string& path) { + for (const char* stage : {"vert", "frag"}) { + const std::string suffix = std::string("_gl.") + stage; + if (!path.ends_with(suffix)) continue; + std::string generated = + path.substr(0, path.size() - suffix.size()) + "." + stage + ".glsl"; + std::error_code ec; + if (std::filesystem::exists(generated, ec)) return generated; + } + return path; +} + std::string OpenGLShader::ReadFile(const std::string& path) { std::ifstream file(path); if (!file.is_open()) { From 8745bd959081e4ee331882b292cd2e97799aff86 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 21:28:05 +0300 Subject: [PATCH 3/5] refactor: drop hand-written gl variants of generated shaders --- assets/shaders/debug_line_gl.frag | 8 --- assets/shaders/debug_line_gl.vert | 18 ----- assets/shaders/ibl_brdf_lut_gl.frag | 96 --------------------------- assets/shaders/ibl_brdf_lut_gl.vert | 9 --- assets/shaders/ibl_irradiance_gl.frag | 46 ------------- assets/shaders/ibl_irradiance_gl.vert | 14 ---- assets/shaders/ibl_prefilter_gl.frag | 95 -------------------------- assets/shaders/ibl_prefilter_gl.vert | 16 ----- assets/shaders/lighting_pass_gl.vert | 18 ----- 9 files changed, 320 deletions(-) delete mode 100644 assets/shaders/debug_line_gl.frag delete mode 100644 assets/shaders/debug_line_gl.vert delete mode 100644 assets/shaders/ibl_brdf_lut_gl.frag delete mode 100644 assets/shaders/ibl_brdf_lut_gl.vert delete mode 100644 assets/shaders/ibl_irradiance_gl.frag delete mode 100644 assets/shaders/ibl_irradiance_gl.vert delete mode 100644 assets/shaders/ibl_prefilter_gl.frag delete mode 100644 assets/shaders/ibl_prefilter_gl.vert delete mode 100644 assets/shaders/lighting_pass_gl.vert diff --git a/assets/shaders/debug_line_gl.frag b/assets/shaders/debug_line_gl.frag deleted file mode 100644 index e513040f..00000000 --- a/assets/shaders/debug_line_gl.frag +++ /dev/null @@ -1,8 +0,0 @@ -#version 450 core - -in vec4 fragColor; -out vec4 outColor; - -void main() { - outColor = fragColor; -} diff --git a/assets/shaders/debug_line_gl.vert b/assets/shaders/debug_line_gl.vert deleted file mode 100644 index 9dd6cf58..00000000 --- a/assets/shaders/debug_line_gl.vert +++ /dev/null @@ -1,18 +0,0 @@ -#version 450 core - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec4 inTangent; -layout(location = 3) in vec4 inColor; -layout(location = 4) in vec2 inUV; - -layout(std140, binding = 0) uniform DebugLineUBO { - mat4 ViewProjection; -}; - -out vec4 fragColor; - -void main() { - fragColor = inColor; - gl_Position = ViewProjection * vec4(inPosition, 1.0); -} diff --git a/assets/shaders/ibl_brdf_lut_gl.frag b/assets/shaders/ibl_brdf_lut_gl.frag deleted file mode 100644 index 8e7b8fd8..00000000 --- a/assets/shaders/ibl_brdf_lut_gl.frag +++ /dev/null @@ -1,96 +0,0 @@ -#version 450 core - -// ============================================================ -// IBL BRDF Integration LUT Generator - OpenGL Fragment -// Precomputes split-sum BRDF lookup table -// ============================================================ - -in vec2 fragUV; -out vec2 outColor; - -const float PI = 3.14159265359; - -float RadicalInverse_VdC(uint bits) { - bits = (bits << 16u) | (bits >> 16u); - bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); - bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); - bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); - bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); - return float(bits) * 2.3283064365386963e-10; -} - -vec2 Hammersley(uint i, uint N) { - return vec2(float(i) / float(N), RadicalInverse_VdC(i)); -} - -vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { - float a = roughness * roughness; - - float phi = 2.0 * PI * Xi.x; - float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); - float sinTheta = sqrt(1.0 - cosTheta * cosTheta); - - vec3 H; - H.x = cos(phi) * sinTheta; - H.y = sin(phi) * sinTheta; - H.z = cosTheta; - - vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); - vec3 tangent = normalize(cross(up, N)); - vec3 bitangent = cross(N, tangent); - - return normalize(tangent * H.x + bitangent * H.y + N * H.z); -} - -float GeometrySchlickGGX_IBL(float NdotV, float roughness) { - float k = (roughness * roughness) / 2.0; - return NdotV / (NdotV * (1.0 - k) + k); -} - -float GeometrySmith_IBL(vec3 N, vec3 V, vec3 L, float roughness) { - float NdotV = max(dot(N, V), 0.0); - float NdotL = max(dot(N, L), 0.0); - float ggx2 = GeometrySchlickGGX_IBL(NdotV, roughness); - float ggx1 = GeometrySchlickGGX_IBL(NdotL, roughness); - return ggx1 * ggx2; -} - -vec2 IntegrateBRDF(float NdotV, float roughness) { - vec3 V; - V.x = sqrt(1.0 - NdotV * NdotV); - V.y = 0.0; - V.z = NdotV; - - float A = 0.0; - float B = 0.0; - - vec3 N = vec3(0.0, 0.0, 1.0); - - const uint SAMPLE_COUNT = 1024u; - for (uint i = 0u; i < SAMPLE_COUNT; ++i) { - vec2 Xi = Hammersley(i, SAMPLE_COUNT); - vec3 H = ImportanceSampleGGX(Xi, N, roughness); - vec3 L = normalize(2.0 * dot(V, H) * H - V); - - float NdotL = max(L.z, 0.0); - float NdotH = max(H.z, 0.0); - float VdotH = max(dot(V, H), 0.0); - - if (NdotL > 0.0) { - float G = GeometrySmith_IBL(N, V, L, roughness); - float G_Vis = (G * VdotH) / (NdotH * NdotV); - float Fc = pow(1.0 - VdotH, 5.0); - - A += (1.0 - Fc) * G_Vis; - B += Fc * G_Vis; - } - } - - A /= float(SAMPLE_COUNT); - B /= float(SAMPLE_COUNT); - return vec2(A, B); -} - -void main() { - outColor = IntegrateBRDF(fragUV.x, fragUV.y); -} diff --git a/assets/shaders/ibl_brdf_lut_gl.vert b/assets/shaders/ibl_brdf_lut_gl.vert deleted file mode 100644 index 0156dc23..00000000 --- a/assets/shaders/ibl_brdf_lut_gl.vert +++ /dev/null @@ -1,9 +0,0 @@ -#version 450 core - -out vec2 fragUV; - -void main() { - vec2 uv = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); - gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0); - fragUV = uv; -} diff --git a/assets/shaders/ibl_irradiance_gl.frag b/assets/shaders/ibl_irradiance_gl.frag deleted file mode 100644 index b78168a1..00000000 --- a/assets/shaders/ibl_irradiance_gl.frag +++ /dev/null @@ -1,46 +0,0 @@ -#version 450 core - -// ============================================================ -// IBL Irradiance Convolution - OpenGL Fragment -// ============================================================ - -in vec3 fragLocalPos; -out vec4 outColor; - -uniform samplerCube environmentMap; - -const float PI = 3.14159265359; - -void main() { - vec3 N = normalize(fragLocalPos); - - vec3 irradiance = vec3(0.0); - - vec3 up = vec3(0.0, 1.0, 0.0); - vec3 right = normalize(cross(up, N)); - up = normalize(cross(N, right)); - - float sampleDelta = 0.025; - float nrSamples = 0.0; - - for (float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { - for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { - vec3 tangentSample = vec3( - sin(theta) * cos(phi), - sin(theta) * sin(phi), - cos(theta)); - - vec3 sampleVec = tangentSample.x * right - + tangentSample.y * up - + tangentSample.z * N; - - irradiance += texture(environmentMap, sampleVec).rgb - * cos(theta) * sin(theta); - nrSamples++; - } - } - - irradiance = PI * irradiance * (1.0 / nrSamples); - - outColor = vec4(irradiance, 1.0); -} diff --git a/assets/shaders/ibl_irradiance_gl.vert b/assets/shaders/ibl_irradiance_gl.vert deleted file mode 100644 index 655abb79..00000000 --- a/assets/shaders/ibl_irradiance_gl.vert +++ /dev/null @@ -1,14 +0,0 @@ -#version 450 core - -layout(location = 0) in vec3 inPosition; - -layout(std140, binding = 0) uniform FaceUBO { - mat4 ViewProjection; -}; - -out vec3 fragLocalPos; - -void main() { - fragLocalPos = inPosition; - gl_Position = ViewProjection * vec4(inPosition, 1.0); -} diff --git a/assets/shaders/ibl_prefilter_gl.frag b/assets/shaders/ibl_prefilter_gl.frag deleted file mode 100644 index 405ba195..00000000 --- a/assets/shaders/ibl_prefilter_gl.frag +++ /dev/null @@ -1,95 +0,0 @@ -#version 450 core - -// ============================================================ -// IBL Prefilter Environment Map - OpenGL Fragment -// ============================================================ - -in vec3 fragLocalPos; -out vec4 outColor; - -layout(std140, binding = 0) uniform PrefilterUBO { - mat4 ViewProjection; - float Roughness; - float _pfPad0, _pfPad1, _pfPad2; -}; - -uniform samplerCube environmentMap; - -const float PI = 3.14159265359; - -float RadicalInverse_VdC(uint bits) { - bits = (bits << 16u) | (bits >> 16u); - bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); - bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); - bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); - bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); - return float(bits) * 2.3283064365386963e-10; -} - -vec2 Hammersley(uint i, uint N) { - return vec2(float(i) / float(N), RadicalInverse_VdC(i)); -} - -vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { - float a = roughness * roughness; - - float phi = 2.0 * PI * Xi.x; - float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); - float sinTheta = sqrt(1.0 - cosTheta * cosTheta); - - vec3 H; - H.x = cos(phi) * sinTheta; - H.y = sin(phi) * sinTheta; - H.z = cosTheta; - - vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); - vec3 tangent = normalize(cross(up, N)); - vec3 bitangent = cross(N, tangent); - - return normalize(tangent * H.x + bitangent * H.y + N * H.z); -} - -float DistributionGGX(float NdotH, float roughness) { - float a = roughness * roughness; - float a2 = a * a; - float NdotH2 = NdotH * NdotH; - float denom = NdotH2 * (a2 - 1.0) + 1.0; - denom = PI * denom * denom; - return a2 / max(denom, 0.0000001); -} - -void main() { - vec3 N = normalize(fragLocalPos); - vec3 R = N; - vec3 V = R; - - const uint SAMPLE_COUNT = 1024u; - float totalWeight = 0.0; - vec3 prefilteredColor = vec3(0.0); - - for (uint i = 0u; i < SAMPLE_COUNT; ++i) { - vec2 Xi = Hammersley(i, SAMPLE_COUNT); - vec3 H = ImportanceSampleGGX(Xi, N, Roughness); - vec3 L = normalize(2.0 * dot(V, H) * H - V); - - float NdotL = max(dot(N, L), 0.0); - if (NdotL > 0.0) { - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); - float D = DistributionGGX(NdotH, Roughness); - float pdf = D * NdotH / (4.0 * HdotV) + 0.0001; - - float resolution = 512.0; - float saTexel = 4.0 * PI / (6.0 * resolution * resolution); - float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001); - float mipLevel = Roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); - - prefilteredColor += textureLod(environmentMap, L, mipLevel).rgb * NdotL; - totalWeight += NdotL; - } - } - - prefilteredColor /= max(totalWeight, 0.001); - - outColor = vec4(prefilteredColor, 1.0); -} diff --git a/assets/shaders/ibl_prefilter_gl.vert b/assets/shaders/ibl_prefilter_gl.vert deleted file mode 100644 index 0670706a..00000000 --- a/assets/shaders/ibl_prefilter_gl.vert +++ /dev/null @@ -1,16 +0,0 @@ -#version 450 core - -layout(location = 0) in vec3 inPosition; - -layout(std140, binding = 0) uniform PrefilterUBO { - mat4 ViewProjection; - float Roughness; - float _pfPad0, _pfPad1, _pfPad2; -}; - -out vec3 fragLocalPos; - -void main() { - fragLocalPos = inPosition; - gl_Position = ViewProjection * vec4(inPosition, 1.0); -} diff --git a/assets/shaders/lighting_pass_gl.vert b/assets/shaders/lighting_pass_gl.vert deleted file mode 100644 index 9860cad3..00000000 --- a/assets/shaders/lighting_pass_gl.vert +++ /dev/null @@ -1,18 +0,0 @@ -#version 450 core - -// Fullscreen triangle — no vertex buffer required. -// Call glDrawArrays(GL_TRIANGLES, 0, 3) with an empty VAO. -out vec2 fragUV; - -void main() { - // Vertex positions for a triangle that covers the entire screen: - // 0: (-1,-1) 1: (3,-1) 2: (-1, 3) - vec2 positions[3] = vec2[3]( - vec2(-1.0, -1.0), - vec2( 3.0, -1.0), - vec2(-1.0, 3.0) - ); - vec2 pos = positions[gl_VertexID]; - gl_Position = vec4(pos, 0.0, 1.0); - fragUV = pos * 0.5 + 0.5; -} From 66a498eab56b9624cc4341bbf98129e977c0129e Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 21:28:12 +0300 Subject: [PATCH 4/5] ci: compile the generated gl and hlsl shaders --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f289cd9..e4530030 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,32 @@ jobs: exit 1 fi + # Nothing loads the generated GL or HLSL until runtime, so compile them here. + - name: Generated GL shaders compile + if: runner.os == 'Linux' + run: | + failed=0 + for f in build/shaders/*.glsl; do + stage="${f%.glsl}"; stage="${stage##*.}" + glslc --target-env=opengl -fauto-map-locations -fauto-bind-uniforms \ + -fshader-stage="$stage" "$f" -o /dev/null || failed=1 + done + exit $failed + + - name: Generated HLSL compiles with fxc + if: runner.os == 'Windows' + shell: pwsh + run: | + $fxc = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\fxc.exe" | + Sort-Object FullName | Select-Object -Last 1 + $failed = 0 + foreach ($f in Get-ChildItem build/shaders/*.hlsl) { + $target = if ($f.Name -like '*.vert.hlsl') { 'vs_5_0' } else { 'ps_5_0' } + & $fxc.FullName /nologo /T $target /E main /Fo NUL $f.FullName + if ($LASTEXITCODE -ne 0) { $failed = 1 } + } + exit $failed + - name: Report ccache statistics if: runner.os == 'Linux' run: ccache -s | head -8 From 9a1b1826867759142077fe81aa5b4523544c19d6 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 21:28:12 +0300 Subject: [PATCH 5/5] docs: describe generated shader variants --- docs/pages/shader_authoring.md | 64 +++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/docs/pages/shader_authoring.md b/docs/pages/shader_authoring.md index 526c15da..8f7ec600 100644 --- a/docs/pages/shader_authoring.md +++ b/docs/pages/shader_authoring.md @@ -2,14 +2,16 @@ Four backends means four shading languages. A single logical shader such as `default_shader` exists on disk as up to eight files, and the engine picks -between them by rewriting the file extension at load time. Nothing -cross-compiles, and nothing falls back, so a variant you forget to write is -a pass that silently stops drawing on that backend. +between them by rewriting the file extension at load time. A growing set of +shaders is authored once as Vulkan GLSL and the build generates the OpenGL +and HLSL text from its SPIR-V (see section 4). Everything else is still +written by hand per backend, so a variant you forget to write is a pass that +silently stops drawing on that backend. | Backend | Files for stem `foo` | Compiled | | :--- | :--- | :--- | | Vulkan | `foo.vert`, `foo.frag`, plus `foo.vert.spv`, `foo.frag.spv` | Ahead of time by `glslc`, committed as `.spv` | -| OpenGL | `foo_gl.vert`, `foo_gl.frag` | At load, by the GL driver | +| OpenGL | `foo.vert.glsl`, `foo.frag.glsl` when generated, else `foo_gl.vert`, `foo_gl.frag` | Generated at build time by SPIRV-Cross, or hand-written; compiled at load by the GL driver | | DirectX 11 | `foo.hlsl` | At load, by `D3DCompileFromFile` | | DirectX 12 | `foo_dx12.hlsl` | At load, by `D3DCompileFromFile` | @@ -143,10 +145,44 @@ first that the variant file exists under the exact expected name. --- -## 4. GLSL for OpenGL - -OpenGL loads `foo_gl.vert` and `foo_gl.frag` as GLSL text and compiles them -in the driver. Its error reporting is the best of the four: +## 4. Generated Variants + +Shaders listed in `ENGINE_CROSS_SHADERS` (or `ENGINE_CROSS_SHADERS_FLIP_Y`) +in the engine's `CMakeLists.txt` are authored only as Vulkan GLSL. After +`glslc` produces the `.spv`, `sleak_cross_compile_shader()` runs the +vendored SPIRV-Cross (`vendors/SPIRV-Cross`, built as a host tool) on it and +writes two more files per stage into `/Engine/shaders`, staged next +to the `.spv`: + +- `foo.vert.glsl`, `foo.frag.glsl`: GLSL 4.00 with + `GL_ARB_shading_language_420pack`, so `layout(binding = N)` survives. Push + constant blocks become a `std140` uniform block with no binding, which GL + places at binding 0. Varyings carry no `location`, so a generated stage + links with a hand-written one by name. +- `foo.vert.hlsl`, `foo.frag.hlsl`: Shader Model 5.0, entry point `main`. + Nothing loads these yet. The DirectX backends keep their hand-written + files because their register layout and input semantics differ from what + SPIRV-Cross emits. + +Without `glslc`, the generator runs on the committed `.spv` instead, so a +checkout still gets its GL shaders. A cross compiling build can not run the +vendored tool and needs a host `spirv-cross` (configure fails without one). + +The GL binding of each resource is its Vulkan `binding`, with the `set` +dropped. That only works when the Vulkan layout already matches what the GL +backend binds, so a shader is listed only after checking that. Add +`FLIP_Y` for vertex shaders that negate `gl_Position.y` for Vulkan; +SPIRV-Cross negates it back for GL. + +`OpenGLShader` maps `foo_gl.vert` and `foo_gl.frag` to the generated +`foo.vert.glsl` and `foo.frag.glsl` when they exist, stage by stage, and +otherwise loads the hand-written file. A game that wants to override a +generated engine shader has to ship a file under the generated name. + +## 5. GLSL for OpenGL + +OpenGL loads `foo_gl.vert` and `foo_gl.frag` (or their generated +replacements) as GLSL text and compiles them in the driver. Its error reporting is the best of the four: `Cannot open shader file: {path}` names the missing file, `Failed to read shader files: {vert} and {frag}` names both, and `Shader compilation failed: {log}` and `Shader program linking failed: {log}` @@ -158,7 +194,7 @@ is not valid SPIR-V input. --- -## 5. Custom Vertex Formats and Shader Stems +## 6. Custom Vertex Formats and Shader Stems A game that registers its own vertex layout names its shaders through `Sleak::VertexLayoutDesc`: @@ -225,7 +261,7 @@ recreation or shutdown, so a resize re-attempts compilation. --- -## 6. Keeping Variants in Sync +## 7. Keeping Variants in Sync Grep for the stem before you touch anything: @@ -244,7 +280,9 @@ backends whose variant is absent, which matches what A working checklist for a new shader stem: 1. Write `foo.vert` and `foo.frag` (Vulkan GLSL). -2. Write `foo_gl.vert` and `foo_gl.frag` (desktop GLSL). +2. If the GL backend can bind it with the Vulkan bindings, add it to + `ENGINE_CROSS_SHADERS`. Otherwise write `foo_gl.vert` and `foo_gl.frag` + (desktop GLSL). 3. Write `foo.hlsl` and `foo_dx12.hlsl` with `VS_Main` and `PS_Main`. 4. Build, or run the project's shader script, to produce `foo.vert.spv` and `foo.frag.spv`. @@ -258,12 +296,14 @@ not even name the file it wanted. --- -## 7. Where to Look in the Source +## 8. Where to Look in the Source | Question | File | | :--- | :--- | | Extension rewrite per backend | The single-argument `compile()` in each `src/Graphics//` shader class | | SPIR-V build target and its scope | `CMakeLists.txt`, the `find_program(GLSLC ...)` block | +| Which shaders are generated, and how | `CMakeLists.txt`, `sleak_cross_compile_shader` and `ENGINE_CROSS_SHADERS` | +| Generated GL path lookup | `src/Graphics/OpenGL/OpenGLShader.cpp`, `ResolveGenerated` | | Stem to pipeline mapping and error text | `src/Graphics/Vulkan/VulkanPipelines.cpp`, `CreateCustomFormatPipelines` | | Sticky failure flags and draw suppression | `include/private/Graphics/Vulkan/VulkanRenderer.hpp`, `CustomFormatPipelines` | | OpenGL attribute binding from a layout | `src/Graphics/OpenGL/OpenGLRenderer.cpp`, `BindVertexBuffer` |