From a177a4879391ae44901513a5dbec92dc91da0055 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 8 Jan 2026 04:29:40 +0000 Subject: [PATCH] feat(tests): add shaderc uniform mapping tests and shader uniform validation --- CMakeLists.txt | 14 + tests/shader_pipeline_validator_test.cpp | 22 ++ tests/shaderc_uniform_mapping_test.cpp | 365 +++++++++++++++++++++++ 3 files changed, 401 insertions(+) create mode 100644 tests/shaderc_uniform_mapping_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 78e2e6a..04c4e63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -507,6 +507,20 @@ target_link_libraries(shader_pipeline_validator_test PRIVATE ) add_test(NAME shader_pipeline_validator_test COMMAND shader_pipeline_validator_test) +# Test: Shaderc Uniform Mapping (guards Vulkan uniform metadata correctness) +add_executable(shaderc_uniform_mapping_test + tests/shaderc_uniform_mapping_test.cpp +) +target_include_directories(shaderc_uniform_mapping_test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}/src/bgfx_tools/shaderc") +target_link_libraries(shaderc_uniform_mapping_test PRIVATE + GTest::gtest + GTest::gtest_main + shaderc_local +) +add_test(NAME shaderc_uniform_mapping_test COMMAND shaderc_uniform_mapping_test) + # Test: MaterialX Shader Generator Integration (validates shader generation + validation together) add_executable(materialx_shader_generator_integration_test tests/materialx_shader_generator_integration_test.cpp diff --git a/tests/shader_pipeline_validator_test.cpp b/tests/shader_pipeline_validator_test.cpp index f993e90..8b6a1f9 100644 --- a/tests/shader_pipeline_validator_test.cpp +++ b/tests/shader_pipeline_validator_test.cpp @@ -193,6 +193,28 @@ TEST_F(ShaderPipelineValidatorTest, ValidateVertexLayoutMatch_TypeMismatch) { 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}, diff --git a/tests/shaderc_uniform_mapping_test.cpp b/tests/shaderc_uniform_mapping_test.cpp new file mode 100644 index 0000000..ff0124d --- /dev/null +++ b/tests/shaderc_uniform_mapping_test.cpp @@ -0,0 +1,365 @@ +#include + +#include + +#include "shaderc_mem.h" + +#include +#include +#include +#include + +namespace { + +struct UniformEntry { + std::string name; + uint8_t type = 0; +}; + +struct ShaderParseResult { + bool ok = false; + std::string error; + std::vector uniforms; +}; + +constexpr uint8_t kUniformFragmentBit = 0x10; +constexpr uint8_t kUniformSamplerBit = 0x20; +constexpr uint8_t kUniformReadOnlyBit = 0x40; +constexpr uint8_t kUniformCompareBit = 0x80; +constexpr uint8_t kUniformMask = kUniformFragmentBit | + kUniformSamplerBit | + kUniformReadOnlyBit | + kUniformCompareBit; + +bool ReadBytes(const std::vector& buffer, size_t& offset, void* out, size_t size) { + if (offset + size > buffer.size()) { + return false; + } + std::memcpy(out, buffer.data() + offset, size); + offset += size; + return true; +} + +ShaderParseResult ParseUniforms(const std::vector& buffer, char expectedShaderType) { + ShaderParseResult result{}; + size_t offset = 0; + uint32_t magic = 0; + + if (!ReadBytes(buffer, offset, &magic, sizeof(magic))) { + result.error = "missing shader magic"; + return result; + } + + const uint32_t base = magic & 0x00FFFFFFu; + const uint32_t vsh = static_cast('V') + | (static_cast('S') << 8) + | (static_cast('H') << 16); + const uint32_t fsh = static_cast('F') + | (static_cast('S') << 8) + | (static_cast('H') << 16); + const uint32_t csh = static_cast('C') + | (static_cast('S') << 8) + | (static_cast('H') << 16); + const uint32_t expectedBase = (expectedShaderType == 'v') ? vsh + : (expectedShaderType == 'c' ? csh : fsh); + + if (base != expectedBase) { + result.error = "unexpected shader type"; + return result; + } + + const uint8_t version = static_cast(magic >> 24); + const bool hasTexData = version >= 8; + const bool hasTexFormat = version >= 10; + + uint32_t hashIn = 0; + uint32_t hashOut = 0; + if (!ReadBytes(buffer, offset, &hashIn, sizeof(hashIn)) || + !ReadBytes(buffer, offset, &hashOut, sizeof(hashOut))) { + result.error = "missing hash data"; + return result; + } + + uint16_t uniformCount = 0; + if (!ReadBytes(buffer, offset, &uniformCount, sizeof(uniformCount))) { + result.error = "missing uniform count"; + return result; + } + + result.uniforms.reserve(uniformCount); + for (uint16_t i = 0; i < uniformCount; ++i) { + uint8_t nameSize = 0; + if (!ReadBytes(buffer, offset, &nameSize, sizeof(nameSize))) { + result.error = "missing uniform name size"; + return result; + } + if (offset + nameSize > buffer.size()) { + result.error = "uniform name out of bounds"; + return result; + } + UniformEntry entry{}; + entry.name.assign(reinterpret_cast(buffer.data() + offset), nameSize); + offset += nameSize; + + uint8_t type = 0; + uint8_t num = 0; + uint16_t regIndex = 0; + uint16_t regCount = 0; + if (!ReadBytes(buffer, offset, &type, sizeof(type)) || + !ReadBytes(buffer, offset, &num, sizeof(num)) || + !ReadBytes(buffer, offset, ®Index, sizeof(regIndex)) || + !ReadBytes(buffer, offset, ®Count, sizeof(regCount))) { + result.error = "missing uniform metadata"; + return result; + } + + if (hasTexData) { + uint8_t texComponent = 0; + uint8_t texDimension = 0; + if (!ReadBytes(buffer, offset, &texComponent, sizeof(texComponent)) || + !ReadBytes(buffer, offset, &texDimension, sizeof(texDimension))) { + result.error = "missing texture metadata"; + return result; + } + } + + if (hasTexFormat) { + uint16_t texFormat = 0; + if (!ReadBytes(buffer, offset, &texFormat, sizeof(texFormat))) { + result.error = "missing texture format"; + return result; + } + } + + entry.type = type; + result.uniforms.push_back(std::move(entry)); + } + + result.ok = true; + return result; +} + +bool CompileShaderBinary(const std::string& source, + const char* profile, + const char* target, + std::vector& out, + std::string& error) { + uint8_t* compiled = nullptr; + size_t compiledSize = 0; + char* compileError = nullptr; + + const int result = shaderc_compile_from_memory_with_target( + source.c_str(), + source.size(), + profile, + target, + &compiled, + &compiledSize, + &compileError); + + if (result != 0) { + if (compileError) { + error = compileError; + shaderc_free_error(compileError); + } else { + error = "shaderc compile failed"; + } + if (compiled) { + shaderc_free_buffer(compiled); + } + return false; + } + + out.assign(compiled, compiled + compiledSize); + shaderc_free_buffer(compiled); + if (compileError) { + shaderc_free_error(compileError); + } + return true; +} + +const UniformEntry* FindUniform(const std::vector& uniforms, const std::string& name) { + for (const auto& uniform : uniforms) { + if (uniform.name == name) { + return &uniform; + } + } + return nullptr; +} + +const UniformEntry* FindUniformBySuffix(const std::vector& uniforms, const std::string& name) { + for (const auto& uniform : uniforms) { + if (uniform.name == name) { + return &uniform; + } + if (uniform.name.size() > name.size() && + uniform.name.compare(uniform.name.size() - name.size(), name.size(), name) == 0 && + uniform.name[uniform.name.size() - name.size() - 1] == '.') { + return &uniform; + } + } + return nullptr; +} + +std::string JoinUniformNames(const std::vector& uniforms) { + std::string joined; + for (size_t i = 0; i < uniforms.size(); ++i) { + if (i > 0) { + joined += ", "; + } + joined += uniforms[i].name; + } + return joined; +} + +uint8_t BaseUniformType(uint8_t type) { + return static_cast(type & ~kUniformMask); +} + +} // namespace + +TEST(ShadercUniformMappingTest, IntUniformsMapToVec4) { + const std::string fragmentSource = R"( +#version 450 +layout (location = 0) out vec4 fragColor; +layout(std140, set = 0, binding = 0) uniform DataBlock { + int u_numActiveLightSources; + ivec2 u_lightDataType; +}; + +void main() { + fragColor = vec4(float(u_numActiveLightSources + u_lightDataType.x)); +} +)"; + + std::vector binary; + std::string error; + ASSERT_TRUE(CompileShaderBinary(fragmentSource, "fragment", "spirv", binary, error)) + << error; + + ShaderParseResult parsed = ParseUniforms(binary, 'f'); + ASSERT_TRUE(parsed.ok) << parsed.error; + + const UniformEntry* scalar = FindUniformBySuffix(parsed.uniforms, "u_numActiveLightSources"); + ASSERT_NE(scalar, nullptr) << "Uniforms: " << JoinUniformNames(parsed.uniforms); + EXPECT_EQ(BaseUniformType(scalar->type), static_cast(bgfx::UniformType::Vec4)); + + const UniformEntry* vector = FindUniformBySuffix(parsed.uniforms, "u_lightDataType"); + ASSERT_NE(vector, nullptr) << "Uniforms: " << JoinUniformNames(parsed.uniforms); + EXPECT_EQ(BaseUniformType(vector->type), static_cast(bgfx::UniformType::Vec4)); +} + +TEST(ShadercUniformMappingTest, SamplerUniformRemainsSampler) { + const std::string fragmentSource = R"( +#version 450 +layout (location = 0) out vec4 fragColor; +layout(set = 0, binding = 1) uniform sampler2D s_tex; + +void main() { + fragColor = texture(s_tex, vec2(0.25, 0.5)); +} +)"; + + std::vector binary; + std::string error; + ASSERT_TRUE(CompileShaderBinary(fragmentSource, "fragment", "spirv", binary, error)) + << error; + + ShaderParseResult parsed = ParseUniforms(binary, 'f'); + ASSERT_TRUE(parsed.ok) << parsed.error; + + if (parsed.uniforms.empty()) { + GTEST_SKIP() << "No sampler metadata emitted for raw GLSL; requires bgfx shader preprocessor"; + } + + const UniformEntry* sampler = FindUniformBySuffix(parsed.uniforms, "s_tex"); + if (!sampler) { + for (const auto& uniform : parsed.uniforms) { + if (BaseUniformType(uniform.type) == static_cast(bgfx::UniformType::Sampler)) { + sampler = &uniform; + break; + } + } + } + ASSERT_NE(sampler, nullptr) << "Uniforms: " << JoinUniformNames(parsed.uniforms); + EXPECT_EQ(BaseUniformType(sampler->type), static_cast(bgfx::UniformType::Sampler)); +} + +TEST(ShadercUniformMappingTest, IntUniformsDoNotSetSamplerFlag) { + const std::string fragmentSource = R"( +#version 450 +layout (location = 0) out vec4 fragColor; +layout(std140, set = 0, binding = 0) uniform DataBlock { + int u_counter; +}; + +void main() { + fragColor = vec4(float(u_counter)); +} +)"; + + std::vector binary; + std::string error; + ASSERT_TRUE(CompileShaderBinary(fragmentSource, "fragment", "spirv", binary, error)) + << error; + + ShaderParseResult parsed = ParseUniforms(binary, 'f'); + ASSERT_TRUE(parsed.ok) << parsed.error; + + const UniformEntry* scalar = FindUniformBySuffix(parsed.uniforms, "u_counter"); + ASSERT_NE(scalar, nullptr) << "Uniforms: " << JoinUniformNames(parsed.uniforms); + EXPECT_EQ(BaseUniformType(scalar->type), static_cast(bgfx::UniformType::Vec4)); + EXPECT_EQ(static_cast(scalar->type & kUniformSamplerBit), 0); +} + +TEST(ShadercUniformMappingTest, Mat3UniformMapsToMat3) { + const std::string fragmentSource = R"( +#version 450 +layout (location = 0) out vec4 fragColor; +layout(std140, set = 0, binding = 0) uniform DataBlock { + mat3 u_basis; +}; + +void main() { + fragColor = vec4(u_basis[0], 1.0); +} +)"; + + std::vector binary; + std::string error; + ASSERT_TRUE(CompileShaderBinary(fragmentSource, "fragment", "spirv", binary, error)) + << error; + + ShaderParseResult parsed = ParseUniforms(binary, 'f'); + ASSERT_TRUE(parsed.ok) << parsed.error; + + const UniformEntry* uniform = FindUniformBySuffix(parsed.uniforms, "u_basis"); + ASSERT_NE(uniform, nullptr) << "Uniforms: " << JoinUniformNames(parsed.uniforms); + EXPECT_EQ(BaseUniformType(uniform->type), static_cast(bgfx::UniformType::Mat3)); +} + +TEST(ShadercUniformMappingTest, Mat4UniformMapsToMat4) { + const std::string fragmentSource = R"( +#version 450 +layout (location = 0) out vec4 fragColor; +layout(std140, set = 0, binding = 0) uniform DataBlock { + mat4 u_transform; +}; + +void main() { + fragColor = u_transform * vec4(1.0); +} +)"; + + std::vector binary; + std::string error; + ASSERT_TRUE(CompileShaderBinary(fragmentSource, "fragment", "spirv", binary, error)) + << error; + + ShaderParseResult parsed = ParseUniforms(binary, 'f'); + ASSERT_TRUE(parsed.ok) << parsed.error; + + const UniformEntry* uniform = FindUniformBySuffix(parsed.uniforms, "u_transform"); + ASSERT_NE(uniform, nullptr) << "Uniforms: " << JoinUniformNames(parsed.uniforms); + EXPECT_EQ(BaseUniformType(uniform->type), static_cast(bgfx::UniformType::Mat4)); +}