#include "services/impl/shader/shader_pipeline_validator.hpp" #include "services/interfaces/i_logger.hpp" #include "core/vertex.hpp" #include #include using namespace sdl3cpp::services; // Mock logger for testing class MockLogger : public ILogger { public: void SetLevel(LogLevel) override {} LogLevel GetLevel() const override { return LogLevel::INFO; } void SetOutputFile(const std::string&) override {} void SetMaxLinesPerFile(size_t) override {} void EnableConsoleOutput(bool) override {} void Log(LogLevel, const std::string&) override {} void Trace(const std::string&) override {} void Trace(const std::string&, const std::string&, const std::string&, const std::string&) override {} void Debug(const std::string&) override {} void Info(const std::string&) override {} void Warn(const std::string&) override {} void Error(const std::string&) override {} void TraceFunction(const std::string&) override {} void TraceVariable(const std::string&, const std::string&) override {} void TraceVariable(const std::string&, int) override {} void TraceVariable(const std::string&, size_t) override {} void TraceVariable(const std::string&, bool) override {} void TraceVariable(const std::string&, float) override {} void TraceVariable(const std::string&, double) override {} }; class ShaderPipelineValidatorTest : public ::testing::Test { protected: void SetUp() override { logger = std::make_shared(); validator = std::make_unique(logger); } std::shared_ptr logger; std::unique_ptr validator; }; // ============================================================================ // Test: Extract Shader Inputs // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ExtractShaderInputs_ValidGLSL) { std::string glsl = R"( #version 450 layout (location = 0) in vec3 i_position; layout (location = 1) in vec3 i_normal; layout (location = 2) in vec3 i_tangent; layout (location = 3) in vec2 i_texcoord_0; void main() { gl_Position = vec4(i_position, 1.0); } )"; auto inputs = validator->ExtractShaderInputs(glsl); ASSERT_EQ(inputs.size(), 4); EXPECT_EQ(inputs[0].location, 0); EXPECT_EQ(inputs[0].type, "vec3"); EXPECT_EQ(inputs[0].name, "i_position"); EXPECT_EQ(inputs[0].sizeBytes, 12); EXPECT_EQ(inputs[1].location, 1); EXPECT_EQ(inputs[1].type, "vec3"); EXPECT_EQ(inputs[1].name, "i_normal"); EXPECT_EQ(inputs[2].location, 2); EXPECT_EQ(inputs[2].type, "vec3"); EXPECT_EQ(inputs[2].name, "i_tangent"); EXPECT_EQ(inputs[3].location, 3); EXPECT_EQ(inputs[3].type, "vec2"); EXPECT_EQ(inputs[3].name, "i_texcoord_0"); EXPECT_EQ(inputs[3].sizeBytes, 8); } TEST_F(ShaderPipelineValidatorTest, ExtractShaderInputs_CompactLayout) { std::string glsl = R"( layout(location=0)in vec3 pos; layout(location=1)in vec2 uv; )"; auto inputs = validator->ExtractShaderInputs(glsl); ASSERT_EQ(inputs.size(), 2); EXPECT_EQ(inputs[0].location, 0); EXPECT_EQ(inputs[1].location, 1); } TEST_F(ShaderPipelineValidatorTest, ExtractShaderInputs_NoInputs) { std::string glsl = R"( #version 450 void main() { gl_Position = vec4(0.0); } )"; auto inputs = validator->ExtractShaderInputs(glsl); EXPECT_EQ(inputs.size(), 0); } // ============================================================================ // Test: Extract Shader Outputs // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ExtractShaderOutputs_Valid) { std::string glsl = R"( #version 450 layout (location = 0) out vec3 v_normal; layout (location = 1) out vec2 v_texcoord; void main() { v_normal = vec3(0.0); v_texcoord = vec2(0.0); } )"; auto outputs = validator->ExtractShaderOutputs(glsl); ASSERT_EQ(outputs.size(), 2); EXPECT_EQ(outputs[0].location, 0); EXPECT_EQ(outputs[0].name, "v_normal"); EXPECT_EQ(outputs[1].location, 1); EXPECT_EQ(outputs[1].name, "v_texcoord"); } // ============================================================================ // Test: Validate Vertex Layout Match // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ValidateVertexLayoutMatch_Perfect) { std::vector shaderInputs = { {0, "vec3", "i_position", 12}, {1, "vec3", "i_normal", 12}, {2, "vec2", "i_texcoord", 8}, }; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, {2, "vec2", "TexCoord", 8}, }; auto result = validator->ValidateVertexLayoutMatch(shaderInputs, layoutAttribs, "test_shader"); EXPECT_TRUE(result.passed); EXPECT_EQ(result.errors.size(), 0); } TEST_F(ShaderPipelineValidatorTest, ValidateVertexLayoutMatch_MissingAttribute) { std::vector shaderInputs = { {0, "vec3", "i_position", 12}, {1, "vec3", "i_normal", 12}, {2, "vec3", "i_tangent", 12}, // Shader expects tangent }; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, // Missing tangent at location 2! }; auto result = validator->ValidateVertexLayoutMatch(shaderInputs, layoutAttribs, "test_shader"); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("location 2") != std::string::npos); EXPECT_TRUE(result.errors[0].find("i_tangent") != std::string::npos); } TEST_F(ShaderPipelineValidatorTest, ValidateVertexLayoutMatch_TypeMismatch) { std::vector shaderInputs = { {0, "vec3", "i_position", 12}, {1, "vec4", "i_normal", 16}, // Expects vec4 }; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, // Provides vec3 }; auto result = validator->ValidateVertexLayoutMatch(shaderInputs, layoutAttribs, "test_shader"); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("Type mismatch") != std::string::npos); } TEST_F(ShaderPipelineValidatorTest, ValidateVertexLayoutMatch_ShuffledLocations) { std::vector shaderInputs = { {0, "vec3", "i_position", 12}, {1, "vec2", "i_texcoord_0", 8}, // Wrong location/type for bgfx layout. {2, "vec3", "i_tangent", 12}, {3, "vec3", "i_normal", 12}, // Normal moved to location 3. }; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, {2, "vec3", "Tangent", 12}, {3, "vec2", "TexCoord0", 8}, }; auto result = validator->ValidateVertexLayoutMatch(shaderInputs, layoutAttribs, "test_shader"); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("location 1") != std::string::npos); } TEST_F(ShaderPipelineValidatorTest, ValidateVertexLayoutMatch_UnusedAttribute) { std::vector shaderInputs = { {0, "vec3", "i_position", 12}, }; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, // Layout provides but shader doesn't use {2, "vec2", "TexCoord", 8}, }; auto result = validator->ValidateVertexLayoutMatch(shaderInputs, layoutAttribs, "test_shader"); EXPECT_TRUE(result.passed); // Not an error, just warning EXPECT_EQ(result.warnings.size(), 2); // 2 unused attributes } // ============================================================================ // Test: Validate Vertex Stride // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ValidateVertexStride_Correct) { std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, {2, "vec3", "Tangent", 12}, {3, "vec2", "TexCoord", 8}, {4, "vec3", "Color", 12}, }; size_t actualSize = sizeof(sdl3cpp::core::Vertex); // Should be 56 auto result = validator->ValidateVertexStride(layoutAttribs, actualSize); EXPECT_TRUE(result.passed); EXPECT_EQ(result.errors.size(), 0); } TEST_F(ShaderPipelineValidatorTest, ValidateVertexStride_Mismatch) { std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, {2, "vec2", "TexCoord", 8}, }; size_t actualSize = 56; // Actual struct is 56 but layout expects 32 auto result = validator->ValidateVertexStride(layoutAttribs, actualSize); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("stride mismatch") != std::string::npos); } // ============================================================================ // Test: Validate Interface Matching // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ValidateInterfaceMatching_Perfect) { std::vector vsOutputs = { {0, "vec3", "v_normal", 12}, {1, "vec2", "v_texcoord", 8}, }; std::vector fsInputs = { {0, "vec3", "v_normal", 12}, {1, "vec2", "v_texcoord", 8}, }; auto result = validator->ValidateInterfaceMatching(vsOutputs, fsInputs, "vs", "fs"); EXPECT_TRUE(result.passed); EXPECT_EQ(result.errors.size(), 0); } TEST_F(ShaderPipelineValidatorTest, ValidateInterfaceMatching_MissingOutput) { std::vector vsOutputs = { {0, "vec3", "v_normal", 12}, // Missing location 1! }; std::vector fsInputs = { {0, "vec3", "v_normal", 12}, {1, "vec2", "v_texcoord", 8}, // Fragment expects this }; auto result = validator->ValidateInterfaceMatching(vsOutputs, fsInputs, "vs", "fs"); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("location 1") != std::string::npos); } TEST_F(ShaderPipelineValidatorTest, ValidateInterfaceMatching_TypeMismatch) { std::vector vsOutputs = { {0, "vec3", "v_data", 12}, }; std::vector fsInputs = { {0, "vec4", "v_data", 16}, // Different type! }; auto result = validator->ValidateInterfaceMatching(vsOutputs, fsInputs, "vs", "fs"); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("Type mismatch") != std::string::npos); } // ============================================================================ // Test: Validate SPIR-V Requirements // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ValidateSpirvRequirements_AllHaveLocation) { std::string glsl = R"( #version 450 layout (location = 0) in vec3 i_position; layout (location = 1) in vec3 i_normal; layout (location = 0) out vec4 fragColor; void main() { fragColor = vec4(i_position + i_normal, 1.0); } )"; auto result = validator->ValidateSpirvRequirements(glsl, "test_shader"); EXPECT_TRUE(result.passed); EXPECT_EQ(result.errors.size(), 0); } TEST_F(ShaderPipelineValidatorTest, ValidateSpirvRequirements_MissingLocation) { std::string glsl = R"( #version 450 in vec3 i_position; // Missing layout(location=N)! layout (location = 0) out vec4 fragColor; void main() { fragColor = vec4(i_position, 1.0); } )"; auto result = validator->ValidateSpirvRequirements(glsl, "test_shader"); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 0); EXPECT_TRUE(result.errors[0].find("SPIR-V requires") != std::string::npos); } TEST_F(ShaderPipelineValidatorTest, ValidateSpirvRequirements_BuiltinsIgnored) { std::string glsl = R"( #version 450 layout (location = 0) in vec3 i_position; void main() { gl_Position = vec4(i_position, 1.0); // gl_ built-in should be ignored } )"; auto result = validator->ValidateSpirvRequirements(glsl, "test_shader"); EXPECT_TRUE(result.passed); } // ============================================================================ // Test: Complete Pipeline Validation // ============================================================================ TEST_F(ShaderPipelineValidatorTest, ValidatePipeline_FullSuccess) { std::string vertexShader = R"( #version 450 layout (location = 0) in vec3 i_position; layout (location = 1) in vec3 i_normal; layout (location = 2) in vec3 i_tangent; layout (location = 3) in vec2 i_texcoord_0; layout (location = 0) out vec3 v_normal; layout (location = 1) out vec2 v_texcoord; void main() { gl_Position = vec4(i_position, 1.0); v_normal = i_normal; v_texcoord = i_texcoord_0; } )"; std::string fragmentShader = R"( #version 450 layout (location = 0) in vec3 v_normal; layout (location = 1) in vec2 v_texcoord; layout (location = 0) out vec4 fragColor; void main() { fragColor = vec4(v_normal * 0.5 + 0.5, 1.0); } )"; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, {2, "vec3", "Tangent", 12}, {3, "vec2", "TexCoord0", 8}, {4, "vec3", "Color0", 12}, }; auto result = validator->ValidatePipeline( vertexShader, fragmentShader, layoutAttribs, sizeof(sdl3cpp::core::Vertex), "test_pipeline" ); EXPECT_TRUE(result.passed); EXPECT_EQ(result.errors.size(), 0); } TEST_F(ShaderPipelineValidatorTest, ValidatePipeline_MultipleErrors) { std::string vertexShader = R"( #version 450 layout (location = 0) in vec3 i_position; layout (location = 5) in vec3 i_invalid; // Wrong location! layout (location = 0) out vec3 v_normal; void main() { gl_Position = vec4(i_position, 1.0); v_normal = vec3(0.0); } )"; std::string fragmentShader = R"( #version 450 layout (location = 0) in vec3 v_normal; layout (location = 1) in vec2 v_missing; // VS doesn't output this! layout (location = 0) out vec4 fragColor; void main() { fragColor = vec4(v_normal, 1.0); } )"; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, }; auto result = validator->ValidatePipeline( vertexShader, fragmentShader, layoutAttribs, 32, // Wrong size! Should be 56 "test_pipeline" ); EXPECT_FALSE(result.passed); EXPECT_GT(result.errors.size(), 2); // Should have multiple errors } // ============================================================================ // Test: Real-World MaterialX Scenario // ============================================================================ TEST_F(ShaderPipelineValidatorTest, RealWorld_MaterialXShader) { // This is the actual scenario that was causing crashes std::string vertexShader = R"( #version 450 layout (location = 0) in vec3 i_position; layout (location = 1) in vec3 i_normal; layout (location = 2) in vec3 i_tangent; layout (location = 3) in vec2 i_texcoord_0; layout (location = 0) out vec3 normalWorld; layout (location = 1) out vec3 tangentWorld; layout (location = 2) out vec2 texcoord; uniform mat4 u_worldMatrix; uniform mat4 u_viewProjectionMatrix; void main() { vec4 worldPos = u_worldMatrix * vec4(i_position, 1.0); gl_Position = u_viewProjectionMatrix * worldPos; normalWorld = normalize(mat3(u_worldMatrix) * i_normal); tangentWorld = normalize(mat3(u_worldMatrix) * i_tangent); texcoord = i_texcoord_0; } )"; std::string fragmentShader = R"( #version 450 layout (location = 0) in vec3 normalWorld; layout (location = 1) in vec3 tangentWorld; layout (location = 2) in vec2 texcoord; layout (location = 0) out vec4 fragColor; void main() { vec3 N = normalize(normalWorld); fragColor = vec4(N * 0.5 + 0.5, 1.0); } )"; // This is the expected bgfx layout std::vector bgfxLayout = { {0, "vec3", "Position", 12}, {1, "vec3", "Normal", 12}, {2, "vec3", "Tangent", 12}, {3, "vec2", "TexCoord0", 8}, {4, "vec3", "Color0", 12}, }; auto result = validator->ValidatePipeline( vertexShader, fragmentShader, bgfxLayout, sizeof(sdl3cpp::core::Vertex), "materialx_floor" ); // This should PASS with the fix EXPECT_TRUE(result.passed) << "MaterialX validation should pass after location remapping fix"; EXPECT_EQ(result.errors.size(), 0); } // ============================================================================ // Test: Edge Cases // ============================================================================ TEST_F(ShaderPipelineValidatorTest, EdgeCase_EmptyShaders) { auto result = validator->ValidatePipeline("", "", {}, 0, "empty"); EXPECT_TRUE(result.passed); // Empty is technically valid } TEST_F(ShaderPipelineValidatorTest, EdgeCase_CommentsAndWhitespace) { std::string glsl = R"( // Comment line /* Block comment */ layout (location = 0) in vec3 i_position; // Inline comment // Another comment )"; auto inputs = validator->ExtractShaderInputs(glsl); EXPECT_EQ(inputs.size(), 1); } TEST_F(ShaderPipelineValidatorTest, EdgeCase_NonContinuousLocations) { std::vector shaderInputs = { {0, "vec3", "i_position", 12}, {2, "vec3", "i_normal", 12}, // Gap at location 1! {5, "vec2", "i_texcoord", 8}, // Gap at 3, 4! }; std::vector layoutAttribs = { {0, "vec3", "Position", 12}, {2, "vec3", "Normal", 12}, {5, "vec2", "TexCoord", 8}, }; auto result = validator->ValidateVertexLayoutMatch(shaderInputs, layoutAttribs, "test"); EXPECT_TRUE(result.passed); // Gaps are allowed, just inefficient } // ============================================================================ // Main // ============================================================================ int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }