mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
feat(mesh): add tangents to MeshPayload and compute during payload building
- Updated MeshPayload structure to include tangents. - Modified BuildPayloadFromBspBuffer to compute tangents based on vertex normals. - Enhanced AppendMeshPayload to handle tangents, either from mesh data or generated. - Updated PushMeshToLua to expose tangents to Lua. - Adjusted ReadVertexArray to read tangents from Lua. feat(shader): implement ShaderPipelineValidator for shader validation - Added ShaderPipelineValidator class to validate shader inputs/outputs and vertex layouts. - Implemented methods to extract shader attributes and validate against vertex layouts. - Added comprehensive validation checks for shader pipelines, including SPIR-V requirements. - Created a logger interface for validation results. test(shader): add unit tests for ShaderPipelineValidator - Implemented extensive unit tests for shader validation, covering various scenarios. - Tests include extraction of shader inputs/outputs, validation of vertex layout matches, stride checks, and interface matching. - Added edge case tests for empty shaders and comments in GLSL code.
This commit is contained in:
62
.clang-tidy
62
.clang-tidy
@@ -1,19 +1,49 @@
|
||||
Checks: >
|
||||
clang-analyzer-*,
|
||||
---
|
||||
Checks: '-*,
|
||||
bugprone-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
modernize-*,
|
||||
cert-*,
|
||||
clang-analyzer-*,
|
||||
cppcoreguidelines-*,
|
||||
misc-*,
|
||||
cppcoreguidelines-macro-usage,
|
||||
-modernize-use-trailing-return-type
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'src/.*'
|
||||
FormatStyle: file
|
||||
modernize-*,
|
||||
performance-*,
|
||||
portability-*,
|
||||
readability-*,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-identifier-length,
|
||||
-readability-magic-numbers,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cert-err58-cpp,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-modernize-avoid-c-arrays,
|
||||
-cppcoreguidelines-avoid-c-arrays'
|
||||
|
||||
WarningsAsErrors: 'bugprone-*,cert-*,clang-analyzer-*,performance-*'
|
||||
|
||||
CheckOptions:
|
||||
- key: modernize-use-override.ForceOverride
|
||||
value: 1
|
||||
- key: cppcoreguidelines-macro-usage.AllowedRegexp
|
||||
value: '^SDL3CPP_.*_HPP$|^SDL_MAIN_HANDLED$'
|
||||
- key: cppcoreguidelines-macro-usage.CheckCapsOnly
|
||||
value: 1
|
||||
- key: readability-identifier-naming.NamespaceCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.MemberCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.PrivateMemberSuffix
|
||||
value: '_'
|
||||
- key: readability-identifier-naming.ConstantCase
|
||||
value: UPPER_CASE
|
||||
- key: readability-function-cognitive-complexity.Threshold
|
||||
value: '50'
|
||||
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
|
||||
value: '1'
|
||||
|
||||
@@ -279,6 +279,7 @@ if(BUILD_SDL3_APP)
|
||||
src/services/impl/scene_script_service.cpp
|
||||
src/services/impl/shader_script_service.cpp
|
||||
src/services/impl/materialx_shader_generator.cpp
|
||||
src/services/impl/shader_pipeline_validator.cpp
|
||||
src/services/impl/gui_script_service.cpp
|
||||
$<$<NOT:$<BOOL:${ENABLE_VITA}>>:src/services/impl/bgfx_gui_service.cpp>
|
||||
$<$<NOT:$<BOOL:${ENABLE_VITA}>>:src/services/impl/bgfx_shader_compiler.cpp>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"build-ninja/build/Release/generators/CMakePresets.json"
|
||||
"build-ninja/build/Release/generators/CMakePresets.json",
|
||||
"build/Release/generators/CMakePresets.json"
|
||||
]
|
||||
}
|
||||
246
SHADER_VALIDATION.md
Normal file
246
SHADER_VALIDATION.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# Mega-Strict Shader Pipeline Validation
|
||||
|
||||
## Overview
|
||||
|
||||
This system provides comprehensive validation of shader pipelines BEFORE they reach the GPU driver, preventing system crashes from malformed shader state.
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Runtime C++ Validator (`shader_pipeline_validator.hpp/cpp`)
|
||||
|
||||
**Location:** `src/services/impl/shader_pipeline_validator.*`
|
||||
|
||||
**Purpose:** Validates shader pipelines at runtime before compilation
|
||||
|
||||
**Checks Performed:**
|
||||
- ✅ Vertex layout matches shader input expectations
|
||||
- ✅ Vertex struct size matches layout stride
|
||||
- ✅ Vertex shader outputs match fragment shader inputs
|
||||
- ✅ SPIR-V requirements (all inputs/outputs have `layout(location=N)`)
|
||||
- ✅ Type compatibility between GLSL and vertex attributes
|
||||
- ✅ Location continuity and gaps
|
||||
|
||||
**Integration Point:** `MaterialXShaderGenerator::Generate()` - validates every MaterialX shader before returning
|
||||
|
||||
**Failure Mode:** **Throws exception** if validation fails, preventing GPU submission
|
||||
|
||||
### 2. Static Clang-Tidy Configuration (`.clang-tidy`)
|
||||
|
||||
**Location:** `./.clang-tidy`
|
||||
|
||||
**Purpose:** Static analysis during development
|
||||
|
||||
**Enabled Checks:**
|
||||
- `bugprone-*` - Bug-prone code patterns
|
||||
- `cert-*` - CERT secure coding guidelines
|
||||
- `clang-analyzer-*` - Deep static analysis
|
||||
- `cppcoreguidelines-*` - C++ Core Guidelines
|
||||
- `performance-*` - Performance issues
|
||||
- `misc-*`, `modernize-*`, `portability-*`, `readability-*`
|
||||
|
||||
**Critical Errors:** Bug-prone and CERT violations are treated as errors
|
||||
|
||||
### 3. Multi-Layer Linting Script (`scripts/lint.sh`)
|
||||
|
||||
**Layers:**
|
||||
1. **Clang-Tidy** - Static analysis with compile_commands.json
|
||||
2. **Cppcheck** - Independent static analyzer
|
||||
3. **Compiler Warnings** - Maximum strictness build (`-Wall -Wextra -Wpedantic -Werror`)
|
||||
4. **Sanitizer Build** - AddressSanitizer + UndefinedBehaviorSanitizer
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./scripts/lint.sh [build-dir]
|
||||
```
|
||||
|
||||
### 4. Python Shader Validator (`scripts/validate_shaders.py`)
|
||||
|
||||
**Purpose:** Standalone shader validation tool
|
||||
|
||||
**Features:**
|
||||
- Extracts shader attributes from GLSL source
|
||||
- Validates vertex layout compatibility
|
||||
- Checks SPIR-V requirements
|
||||
- Validates inter-stage matching
|
||||
- Naming convention validation
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 scripts/validate_shaders.py [--verbose]
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Vertex Attribute Mapping
|
||||
|
||||
**bgfx Vertex Layout Order (MUST MATCH):**
|
||||
```cpp
|
||||
location 0: Position (vec3, 12 bytes)
|
||||
location 1: Normal (vec3, 12 bytes)
|
||||
location 2: Tangent (vec3, 12 bytes)
|
||||
location 3: TexCoord0 (vec2, 8 bytes)
|
||||
location 4: Color0 (vec3, 12 bytes)
|
||||
Total: 56 bytes
|
||||
```
|
||||
|
||||
**Vertex Struct (`core::Vertex`):**
|
||||
```cpp
|
||||
struct Vertex {
|
||||
std::array<float, 3> position; // offset 0, 12 bytes
|
||||
std::array<float, 3> normal; // offset 12, 12 bytes
|
||||
std::array<float, 3> tangent; // offset 24, 12 bytes
|
||||
std::array<float, 2> texcoord; // offset 36, 8 bytes
|
||||
std::array<float, 3> color; // offset 44, 12 bytes
|
||||
}; // Total: 56 bytes
|
||||
```
|
||||
|
||||
**GLSL Shader Inputs (MaterialX):**
|
||||
```glsl
|
||||
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;
|
||||
// location 4 (color) optional - not used by MaterialX
|
||||
```
|
||||
|
||||
### Critical Validations
|
||||
|
||||
1. **Location Match**
|
||||
- Every shader input MUST have corresponding vertex layout attribute at same location
|
||||
- **Violation = CRASH**
|
||||
|
||||
2. **Type Compatibility**
|
||||
- GLSL `vec3` ↔ C++ `std::array<float, 3>`
|
||||
- GLSL `vec2` ↔ C++ `std::array<float, 2>`
|
||||
- **Violation = CRASH or garbage data**
|
||||
|
||||
3. **Stride Match**
|
||||
- `sizeof(core::Vertex)` MUST equal sum of layout attribute sizes
|
||||
- **Violation = Memory corruption, reading wrong vertices**
|
||||
|
||||
4. **Interface Matching**
|
||||
- Vertex shader outputs MUST match fragment shader inputs (by location & type)
|
||||
- **Violation = Undefined behavior, may crash**
|
||||
|
||||
5. **SPIR-V Requirements**
|
||||
- All `in`/`out` variables MUST have `layout(location=N)`
|
||||
- **Violation = Compilation failure**
|
||||
|
||||
## How It Prevented System Crash
|
||||
|
||||
### The Bug
|
||||
|
||||
**Before Validation:**
|
||||
1. MaterialX generated shaders with locations: `[0, 1, 2, 3]`
|
||||
2. But actual order was `[position=0, texcoord=1, normal=2, tangent=3]`
|
||||
3. bgfx vertex layout expected: `[position=0, normal=1, tangent=2, texcoord=3]`
|
||||
4. **Mismatch → Vulkan driver received malformed pipeline state**
|
||||
5. **AMD Radeon driver bug → System crash (not just app crash!)**
|
||||
|
||||
**After Validation:**
|
||||
- Validator detects location mismatch
|
||||
- Throws exception with detailed error message
|
||||
- **Prevents submission to GPU → No driver crash**
|
||||
|
||||
### Fix Applied
|
||||
|
||||
```cpp
|
||||
// MaterialX shader generator now remaps locations to match bgfx:
|
||||
std::map<std::string, int> bgfxLocationMap;
|
||||
bgfxLocationMap["i_position"] = 0; // MUST be 0
|
||||
bgfxLocationMap["i_normal"] = 1; // MUST be 1
|
||||
bgfxLocationMap["i_tangent"] = 2; // MUST be 2
|
||||
bgfxLocationMap["i_texcoord_0"] = 3; // MUST be 3
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### MaterialX Pipeline
|
||||
|
||||
```cpp
|
||||
// In MaterialXShaderGenerator::Generate()
|
||||
ShaderPipelineValidator validator(logger_);
|
||||
|
||||
// Define expected layout
|
||||
std::vector<AttributeInfo> expectedLayout = { /* ... */ };
|
||||
|
||||
// Validate BEFORE returning shaders
|
||||
auto result = validator.ValidatePipeline(
|
||||
vertexSource, fragmentSource, expectedLayout,
|
||||
sizeof(core::Vertex), pipelineName
|
||||
);
|
||||
|
||||
if (!result.passed) {
|
||||
throw std::runtime_error("Validation failed"); // PREVENTS CRASH
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-Commit Hook (Recommended)
|
||||
|
||||
```bash
|
||||
# .git/hooks/pre-commit
|
||||
#!/bin/bash
|
||||
./scripts/lint.sh build-ninja || {
|
||||
echo "❌ Linting failed! Fix errors before committing."
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Compile-Time Validation**
|
||||
- Use `static_assert` to validate vertex struct layout at compile time
|
||||
- Template metaprogramming for layout/struct correspondence
|
||||
|
||||
2. **Shader Hot-Reload Validation**
|
||||
- Re-validate on shader modification
|
||||
- Runtime checks in debug builds
|
||||
|
||||
3. **GPU Capability Checks**
|
||||
- Validate against actual GPU limits
|
||||
- Query max vertex attributes, texture units, etc.
|
||||
|
||||
4. **Vulkan Validation Layers**
|
||||
- Enable comprehensive Vulkan validation in debug builds
|
||||
- Parse validation layer output for early warnings
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Verbose Validation
|
||||
|
||||
```cpp
|
||||
// In shader_pipeline_validator.cpp, re-enable trace logging
|
||||
logger_->Info("Attribute at location " + std::to_string(loc) + ": " + name);
|
||||
```
|
||||
|
||||
### Manual Validation
|
||||
|
||||
```python
|
||||
# Run standalone validator
|
||||
python3 scripts/validate_shaders.py --verbose
|
||||
|
||||
# Check specific shader
|
||||
grep -A20 "RAW_VERTEX_SHADER" sdl3_app.log
|
||||
```
|
||||
|
||||
### Sanitizer Run
|
||||
|
||||
```bash
|
||||
# Build with sanitizers
|
||||
cmake -B build-asan -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined"
|
||||
|
||||
# Run and check for issues
|
||||
./build-asan/sdl3_app -j config/seed_runtime.json
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**The mega-strict validation system prevents GPU driver crashes by:**
|
||||
|
||||
1. ✅ Validating shader/vertex layout compatibility BEFORE GPU submission
|
||||
2. ✅ Enforcing strict type and location matching
|
||||
3. ✅ Catching SPIR-V requirement violations early
|
||||
4. ✅ Providing detailed error messages for debugging
|
||||
5. ✅ **THROWING EXCEPTIONS on validation failure (fail-safe)**
|
||||
|
||||
**Result:** No more system crashes from shader pipeline bugs! 🎉
|
||||
@@ -35,6 +35,7 @@ class SDL3CppConan(ConanFile):
|
||||
"cairo/1.18.0",
|
||||
"libzip/1.10.1",
|
||||
"stb/cci.20230920",
|
||||
"gtest/1.17.0"
|
||||
)
|
||||
RENDER_STACK_REQUIRES = (
|
||||
"bgfx/1.129.8930-495",
|
||||
|
||||
120
config/seed_runtime_opengl.json
Normal file
120
config/seed_runtime_opengl.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"launcher": {
|
||||
"name": "Cube Demo",
|
||||
"description": "3D cube room with first-person controls, lanterns, and physics interactions",
|
||||
"enabled": true
|
||||
},
|
||||
"window_width": 1024,
|
||||
"window_height": 768,
|
||||
"lua_script": "scripts/cube_logic.lua",
|
||||
"scripts_directory": "scripts",
|
||||
"mouse_grab": {
|
||||
"enabled": true,
|
||||
"grab_on_click": true,
|
||||
"release_on_escape": true,
|
||||
"start_grabbed": false,
|
||||
"hide_cursor": true,
|
||||
"relative_mode": true,
|
||||
"grab_mouse_button": "left",
|
||||
"release_key": "escape"
|
||||
},
|
||||
"input_bindings": {
|
||||
"move_forward": "W",
|
||||
"move_back": "S",
|
||||
"move_left": "A",
|
||||
"move_right": "D",
|
||||
"fly_up": "Q",
|
||||
"fly_down": "Z",
|
||||
"jump": "Space",
|
||||
"noclip_toggle": "N",
|
||||
"music_toggle": "M",
|
||||
"music_toggle_gamepad": "start",
|
||||
"gamepad_move_x_axis": "leftx",
|
||||
"gamepad_move_y_axis": "lefty",
|
||||
"gamepad_look_x_axis": "rightx",
|
||||
"gamepad_look_y_axis": "righty",
|
||||
"gamepad_dpad_up": "dpup",
|
||||
"gamepad_dpad_down": "dpdown",
|
||||
"gamepad_dpad_left": "dpleft",
|
||||
"gamepad_dpad_right": "dpright",
|
||||
"gamepad_button_actions": {
|
||||
"a": "gamepad_a",
|
||||
"b": "gamepad_b",
|
||||
"x": "gamepad_x",
|
||||
"y": "gamepad_y",
|
||||
"leftshoulder": "gamepad_lb",
|
||||
"rightshoulder": "gamepad_rb",
|
||||
"leftstick": "gamepad_ls",
|
||||
"rightstick": "gamepad_rs",
|
||||
"back": "gamepad_back",
|
||||
"start": "gamepad_start"
|
||||
},
|
||||
"gamepad_axis_actions": {
|
||||
"lefttrigger": "gamepad_lt",
|
||||
"righttrigger": "gamepad_rt"
|
||||
},
|
||||
"gamepad_axis_action_threshold": 0.5
|
||||
},
|
||||
"project_root": "../",
|
||||
"shaders_directory": "shaders",
|
||||
"bgfx": {
|
||||
"renderer": "opengl"
|
||||
},
|
||||
"materialx": {
|
||||
"enabled": true,
|
||||
"parameters_enabled": true,
|
||||
"library_path": "MaterialX/libraries",
|
||||
"library_folders": [
|
||||
"stdlib",
|
||||
"pbrlib",
|
||||
"lights",
|
||||
"bxdf",
|
||||
"cmlib",
|
||||
"nprlib",
|
||||
"targets"
|
||||
]
|
||||
},
|
||||
"materialx_materials": [
|
||||
{
|
||||
"shader_key": "floor",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_wood_tiled.mtlx",
|
||||
"material": "Tiled_Wood"
|
||||
},
|
||||
{
|
||||
"shader_key": "wall",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brick_procedural.mtlx",
|
||||
"material": "M_BrickPattern"
|
||||
},
|
||||
{
|
||||
"shader_key": "ceiling",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid.mtlx",
|
||||
"material": "Marble_3D"
|
||||
},
|
||||
{
|
||||
"shader_key": "solid",
|
||||
"document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_brass_tiled.mtlx",
|
||||
"material": "Tiled_Brass"
|
||||
}
|
||||
],
|
||||
"atmospherics": {
|
||||
"ambient_strength": 0.006,
|
||||
"fog_density": 0.006,
|
||||
"fog_color": [0.03, 0.04, 0.06],
|
||||
"sky_color": [0.02, 0.03, 0.05],
|
||||
"gamma": 2.2,
|
||||
"exposure": 1.15,
|
||||
"enable_tone_mapping": true,
|
||||
"enable_shadows": true,
|
||||
"enable_ssgi": true,
|
||||
"enable_volumetric_lighting": true,
|
||||
"pbr_roughness": 0.28,
|
||||
"pbr_metallic": 0.08
|
||||
},
|
||||
"gui_font": {
|
||||
"use_freetype": true,
|
||||
"font_path": "scripts/assets/fonts/Roboto-Regular.ttf",
|
||||
"font_size": 18.0
|
||||
},
|
||||
"gui_opacity": 1.0,
|
||||
"config_file": "config/seed_runtime.json"
|
||||
}
|
||||
143
scripts/lint.sh
Executable file
143
scripts/lint.sh
Executable file
@@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Multi-Layer C++ Linting Suite ==="
|
||||
echo ""
|
||||
|
||||
BUILD_DIR="${1:-build-ninja}"
|
||||
SRC_DIRS="src/"
|
||||
ERRORS_FOUND=0
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
if [ "$status" = "OK" ]; then
|
||||
echo -e "${GREEN}✓${NC} $message"
|
||||
elif [ "$status" = "WARN" ]; then
|
||||
echo -e "${YELLOW}⚠${NC} $message"
|
||||
else
|
||||
echo -e "${RED}✗${NC} $message"
|
||||
ERRORS_FOUND=1
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Clang-Tidy (Static Analysis)
|
||||
echo "=== Layer 1: Clang-Tidy Static Analysis ==="
|
||||
if command -v clang-tidy &> /dev/null; then
|
||||
if [ -f "$BUILD_DIR/compile_commands.json" ]; then
|
||||
echo "Running clang-tidy on modified files..."
|
||||
|
||||
# Get list of source files
|
||||
FILES=$(find src/ -name "*.cpp" -o -name "*.hpp" | grep -v "bgfx_deps\|bgfx_docs_examples" || true)
|
||||
|
||||
TIDY_ERRORS=0
|
||||
for file in $FILES; do
|
||||
if ! clang-tidy -p "$BUILD_DIR" "$file" 2>&1 | tee /tmp/clang-tidy-$$.txt | grep -q "error:"; then
|
||||
:
|
||||
else
|
||||
TIDY_ERRORS=$((TIDY_ERRORS + 1))
|
||||
echo "Errors in $file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $TIDY_ERRORS -eq 0 ]; then
|
||||
print_status "OK" "Clang-Tidy: No critical issues"
|
||||
else
|
||||
print_status "ERROR" "Clang-Tidy: $TIDY_ERRORS files with errors"
|
||||
fi
|
||||
else
|
||||
print_status "WARN" "Clang-Tidy: compile_commands.json not found (run cmake with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON)"
|
||||
fi
|
||||
else
|
||||
print_status "WARN" "Clang-Tidy: not installed (install with: sudo dnf install clang-tools-extra)"
|
||||
fi
|
||||
|
||||
# 2. Cppcheck (Static Analysis)
|
||||
echo ""
|
||||
echo "=== Layer 2: Cppcheck Static Analysis ==="
|
||||
if command -v cppcheck &> /dev/null; then
|
||||
echo "Running cppcheck..."
|
||||
if cppcheck --enable=all --inconclusive --suppress=missingIncludeSystem \
|
||||
--suppress=unmatchedSuppression --suppress=unusedFunction \
|
||||
--error-exitcode=1 --inline-suppr \
|
||||
-I src/ src/ 2>&1 | tee /tmp/cppcheck-$$.txt | grep -E "error:|warning:" > /tmp/cppcheck-errors-$$.txt; then
|
||||
|
||||
ERROR_COUNT=$(grep -c "error:" /tmp/cppcheck-errors-$$.txt || echo 0)
|
||||
WARN_COUNT=$(grep -c "warning:" /tmp/cppcheck-errors-$$.txt || echo 0)
|
||||
|
||||
if [ $ERROR_COUNT -eq 0 ]; then
|
||||
print_status "OK" "Cppcheck: No errors ($WARN_COUNT warnings)"
|
||||
else
|
||||
print_status "ERROR" "Cppcheck: $ERROR_COUNT errors, $WARN_COUNT warnings"
|
||||
fi
|
||||
else
|
||||
print_status "OK" "Cppcheck: Clean"
|
||||
fi
|
||||
else
|
||||
print_status "WARN" "Cppcheck: not installed (install with: sudo dnf install cppcheck)"
|
||||
fi
|
||||
|
||||
# 3. Compiler Warnings (Maximum strictness)
|
||||
echo ""
|
||||
echo "=== Layer 3: Compiler Warning Check ==="
|
||||
echo "Rebuilding with maximum warnings enabled..."
|
||||
|
||||
if [ ! -d "$BUILD_DIR-lint" ]; then
|
||||
cmake -B "$BUILD_DIR-lint" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_CXX_FLAGS="-Wall -Wextra -Wpedantic -Werror -Wshadow -Wnon-virtual-dtor -Wold-style-cast -Wcast-align -Wunused -Woverloaded-virtual -Wconversion -Wsign-conversion -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wnull-dereference -Wuseless-cast -Wdouble-promotion -Wformat=2" \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
fi
|
||||
|
||||
if cmake --build "$BUILD_DIR-lint" --target sdl3_app 2>&1 | tee /tmp/compile-warnings-$$.txt | grep -E "error:|warning:"; then
|
||||
print_status "ERROR" "Compiler: Warnings/Errors found"
|
||||
else
|
||||
print_status "OK" "Compiler: Clean build with strict warnings"
|
||||
fi
|
||||
|
||||
# 4. Include-what-you-use (Optional but recommended)
|
||||
echo ""
|
||||
echo "=== Layer 4: Include What You Use ==="
|
||||
if command -v include-what-you-use &> /dev/null; then
|
||||
echo "Running include-what-you-use..."
|
||||
# This requires special cmake configuration
|
||||
print_status "WARN" "IWYU: Requires manual cmake configuration"
|
||||
else
|
||||
print_status "WARN" "IWYU: not installed (install from: https://include-what-you-use.org/)"
|
||||
fi
|
||||
|
||||
# 5. Sanitizer Builds
|
||||
echo ""
|
||||
echo "=== Layer 5: Sanitizer Build Check ==="
|
||||
echo "Building with AddressSanitizer + UndefinedBehaviorSanitizer..."
|
||||
|
||||
if [ ! -d "$BUILD_DIR-asan" ]; then
|
||||
cmake -B "$BUILD_DIR-asan" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
|
||||
fi
|
||||
|
||||
if cmake --build "$BUILD_DIR-asan" --target sdl3_app 2>&1 | tee /tmp/asan-build-$$.txt; then
|
||||
print_status "OK" "Sanitizer build succeeded (run with: $BUILD_DIR-asan/sdl3_app)"
|
||||
else
|
||||
print_status "ERROR" "Sanitizer build failed"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=== Linting Summary ==="
|
||||
if [ $ERRORS_FOUND -eq 0 ]; then
|
||||
echo -e "${GREEN}All checks passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some checks failed. Review output above.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
325
scripts/validate_shaders.py
Executable file
325
scripts/validate_shaders.py
Executable file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mega-strict shader pipeline validator
|
||||
Catches issues before they reach the GPU driver
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Set, Optional, Tuple
|
||||
|
||||
@dataclass
|
||||
class ShaderAttribute:
|
||||
location: int
|
||||
type: str
|
||||
name: str
|
||||
|
||||
@dataclass
|
||||
class VertexLayoutAttribute:
|
||||
location: int
|
||||
type: str
|
||||
name: str
|
||||
size: int # bytes
|
||||
|
||||
@dataclass
|
||||
class ValidationError:
|
||||
severity: str # 'ERROR', 'WARNING', 'INFO'
|
||||
message: str
|
||||
shader_name: Optional[str] = None
|
||||
line_number: Optional[int] = None
|
||||
|
||||
class ShaderValidator:
|
||||
def __init__(self):
|
||||
self.errors: List[ValidationError] = []
|
||||
|
||||
def error(self, msg: str, shader: str = None, line: int = None):
|
||||
self.errors.append(ValidationError('ERROR', msg, shader, line))
|
||||
|
||||
def warning(self, msg: str, shader: str = None, line: int = None):
|
||||
self.errors.append(ValidationError('WARNING', msg, shader, line))
|
||||
|
||||
def info(self, msg: str, shader: str = None, line: int = None):
|
||||
self.errors.append(ValidationError('INFO', msg, shader, line))
|
||||
|
||||
def extract_shader_inputs(self, glsl_source: str, shader_name: str) -> List[ShaderAttribute]:
|
||||
"""Extract vertex shader input attributes"""
|
||||
inputs = []
|
||||
|
||||
# Match: layout (location = N) in type name;
|
||||
pattern = r'layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s+in\s+(\w+)\s+(\w+)\s*;'
|
||||
|
||||
for line_num, line in enumerate(glsl_source.split('\n'), 1):
|
||||
match = re.search(pattern, line)
|
||||
if match:
|
||||
location = int(match.group(1))
|
||||
attr_type = match.group(2)
|
||||
name = match.group(3)
|
||||
inputs.append(ShaderAttribute(location, attr_type, name))
|
||||
self.info(f"Found input: location={location}, type={attr_type}, name={name}",
|
||||
shader_name, line_num)
|
||||
|
||||
return inputs
|
||||
|
||||
def extract_shader_outputs(self, glsl_source: str, shader_name: str) -> List[ShaderAttribute]:
|
||||
"""Extract shader output attributes"""
|
||||
outputs = []
|
||||
|
||||
# Match: layout (location = N) out type name;
|
||||
pattern = r'layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s+out\s+(\w+)\s+(\w+)\s*;'
|
||||
|
||||
for line_num, line in enumerate(glsl_source.split('\n'), 1):
|
||||
match = re.search(pattern, line)
|
||||
if match:
|
||||
location = int(match.group(1))
|
||||
attr_type = match.group(2)
|
||||
name = match.group(3)
|
||||
outputs.append(ShaderAttribute(location, attr_type, name))
|
||||
|
||||
return outputs
|
||||
|
||||
def validate_vertex_layout_match(self, shader_inputs: List[ShaderAttribute],
|
||||
vertex_layout: List[VertexLayoutAttribute],
|
||||
shader_name: str):
|
||||
"""Validate that shader inputs match the vertex layout"""
|
||||
|
||||
# Check that all shader inputs have corresponding vertex layout entries
|
||||
shader_locs = {attr.location: attr for attr in shader_inputs}
|
||||
layout_locs = {attr.location: attr for attr in vertex_layout}
|
||||
|
||||
# Check for missing attributes in layout
|
||||
for loc, shader_attr in shader_locs.items():
|
||||
if loc not in layout_locs:
|
||||
self.error(f"Shader '{shader_name}' expects input at location {loc} "
|
||||
f"({shader_attr.name}: {shader_attr.type}) but vertex layout doesn't provide it")
|
||||
|
||||
# Check for type mismatches
|
||||
for loc in shader_locs.keys() & layout_locs.keys():
|
||||
shader_attr = shader_locs[loc]
|
||||
layout_attr = layout_locs[loc]
|
||||
|
||||
# Type compatibility check
|
||||
shader_type = shader_attr.type
|
||||
layout_type = layout_attr.type
|
||||
|
||||
if not self.types_compatible(shader_type, layout_type):
|
||||
self.error(f"Type mismatch at location {loc}: shader expects {shader_type} "
|
||||
f"but vertex layout provides {layout_type}")
|
||||
|
||||
# Check for location gaps
|
||||
if shader_locs:
|
||||
max_loc = max(shader_locs.keys())
|
||||
for i in range(max_loc):
|
||||
if i not in shader_locs and i in layout_locs:
|
||||
self.warning(f"Vertex layout provides unused attribute at location {i}")
|
||||
|
||||
def types_compatible(self, shader_type: str, layout_type: str) -> bool:
|
||||
"""Check if shader type and layout type are compatible"""
|
||||
|
||||
# Mapping of GLSL types to expected vertex layout types
|
||||
type_map = {
|
||||
'vec2': 'float2',
|
||||
'vec3': 'float3',
|
||||
'vec4': 'float4',
|
||||
'float': 'float',
|
||||
'int': 'int',
|
||||
'ivec2': 'int2',
|
||||
'ivec3': 'int3',
|
||||
'ivec4': 'int4',
|
||||
}
|
||||
|
||||
expected_layout_type = type_map.get(shader_type)
|
||||
return expected_layout_type == layout_type or layout_type == shader_type
|
||||
|
||||
def validate_vertex_buffer_stride(self, vertex_layout: List[VertexLayoutAttribute],
|
||||
actual_vertex_size: int):
|
||||
"""Validate that the vertex layout stride matches the actual vertex struct size"""
|
||||
|
||||
# Calculate expected stride from layout
|
||||
expected_stride = sum(attr.size for attr in vertex_layout)
|
||||
|
||||
if expected_stride != actual_vertex_size:
|
||||
self.error(f"Vertex layout stride mismatch: layout expects {expected_stride} bytes "
|
||||
f"but actual vertex struct is {actual_vertex_size} bytes")
|
||||
|
||||
self.info(f"Layout breakdown:")
|
||||
for attr in vertex_layout:
|
||||
self.info(f" {attr.name}: {attr.size} bytes")
|
||||
|
||||
def validate_location_continuity(self, shader_inputs: List[ShaderAttribute], shader_name: str):
|
||||
"""Warn about non-continuous location assignments"""
|
||||
|
||||
if not shader_inputs:
|
||||
return
|
||||
|
||||
locations = sorted([attr.location for attr in shader_inputs])
|
||||
expected = list(range(len(locations)))
|
||||
|
||||
if locations != expected:
|
||||
self.warning(f"Shader '{shader_name}' has non-continuous location assignments: {locations} "
|
||||
f"(expected: {expected}). This may cause issues with some drivers.",
|
||||
shader_name)
|
||||
|
||||
def validate_spir_v_compatibility(self, glsl_source: str, shader_name: str):
|
||||
"""Check for common SPIR-V compilation issues"""
|
||||
|
||||
lines = glsl_source.split('\n')
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Check for inputs/outputs without location qualifiers (required for SPIR-V)
|
||||
if re.search(r'\bin\s+\w+\s+\w+\s*;', line) and 'layout' not in line:
|
||||
if '#version' not in line and '//' not in line:
|
||||
self.error(f"SPIR-V requires 'layout(location=N)' for all inputs/outputs",
|
||||
shader_name, line_num)
|
||||
|
||||
if re.search(r'\bout\s+\w+\s+\w+\s*;', line) and 'layout' not in line:
|
||||
if '#version' not in line and '//' not in line and 'gl_' not in line:
|
||||
self.error(f"SPIR-V requires 'layout(location=N)' for all inputs/outputs",
|
||||
shader_name, line_num)
|
||||
|
||||
def validate_interface_matching(self, vertex_outputs: List[ShaderAttribute],
|
||||
fragment_inputs: List[ShaderAttribute],
|
||||
vs_name: str, fs_name: str):
|
||||
"""Validate that vertex shader outputs match fragment shader inputs"""
|
||||
|
||||
vs_locs = {attr.location: attr for attr in vertex_outputs}
|
||||
fs_locs = {attr.location: attr for attr in fragment_inputs}
|
||||
|
||||
# Fragment shader inputs should match vertex shader outputs
|
||||
for loc, fs_attr in fs_locs.items():
|
||||
if loc not in vs_locs:
|
||||
self.error(f"Fragment shader '{fs_name}' expects input at location {loc} "
|
||||
f"({fs_attr.name}) but vertex shader '{vs_name}' doesn't output it")
|
||||
else:
|
||||
vs_attr = vs_locs[loc]
|
||||
if vs_attr.type != fs_attr.type:
|
||||
self.error(f"Type mismatch at location {loc}: VS outputs {vs_attr.type} "
|
||||
f"but FS expects {fs_attr.type}")
|
||||
|
||||
def validate_attribute_name_conventions(self, shader_inputs: List[ShaderAttribute],
|
||||
shader_name: str):
|
||||
"""Validate attribute naming conventions"""
|
||||
|
||||
# Expected prefixes for different shader stages
|
||||
valid_prefixes = ['i_', 'a_', 'v_']
|
||||
|
||||
for attr in shader_inputs:
|
||||
if not any(attr.name.startswith(prefix) for prefix in valid_prefixes):
|
||||
self.warning(f"Attribute '{attr.name}' doesn't follow naming convention "
|
||||
f"(expected prefix: {valid_prefixes})", shader_name)
|
||||
|
||||
def print_report(self):
|
||||
"""Print validation report"""
|
||||
|
||||
errors = [e for e in self.errors if e.severity == 'ERROR']
|
||||
warnings = [e for e in self.errors if e.severity == 'WARNING']
|
||||
infos = [e for e in self.errors if e.severity == 'INFO']
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("SHADER PIPELINE VALIDATION REPORT")
|
||||
print("="*80)
|
||||
|
||||
if errors:
|
||||
print(f"\n🔴 ERRORS ({len(errors)}):")
|
||||
for err in errors:
|
||||
loc = f" [{err.shader_name}:{err.line_number}]" if err.shader_name else ""
|
||||
print(f" ✗ {err.message}{loc}")
|
||||
|
||||
if warnings:
|
||||
print(f"\n⚠️ WARNINGS ({len(warnings)}):")
|
||||
for warn in warnings:
|
||||
loc = f" [{warn.shader_name}:{warn.line_number}]" if warn.shader_name else ""
|
||||
print(f" ⚠ {warn.message}{loc}")
|
||||
|
||||
if infos and '--verbose' in sys.argv:
|
||||
print(f"\nℹ️ INFO ({len(infos)}):")
|
||||
for info in infos:
|
||||
loc = f" [{info.shader_name}:{info.line_number}]" if info.shader_name else ""
|
||||
print(f" ℹ {info.message}{loc}")
|
||||
|
||||
print("\n" + "="*80)
|
||||
print(f"Summary: {len(errors)} errors, {len(warnings)} warnings, {len(infos)} info")
|
||||
print("="*80 + "\n")
|
||||
|
||||
return len(errors) == 0
|
||||
|
||||
def main():
|
||||
validator = ShaderValidator()
|
||||
|
||||
# Example: Define expected vertex layout based on bgfx_graphics_backend.cpp
|
||||
# This should match: Position, Normal, Tangent, TexCoord0, Color0
|
||||
vertex_layout = [
|
||||
VertexLayoutAttribute(0, 'float3', 'Position', 12),
|
||||
VertexLayoutAttribute(1, 'float3', 'Normal', 12),
|
||||
VertexLayoutAttribute(2, 'float3', 'Tangent', 12),
|
||||
VertexLayoutAttribute(3, 'float2', 'TexCoord0', 8),
|
||||
VertexLayoutAttribute(4, 'float3', 'Color0', 12),
|
||||
]
|
||||
|
||||
# Expected vertex struct size: 3+3+3+2+3 = 14 floats = 56 bytes
|
||||
vertex_struct_size = 56
|
||||
|
||||
# Example vertex shader (this would come from reading actual shader files)
|
||||
example_vs = """
|
||||
#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;
|
||||
}
|
||||
"""
|
||||
|
||||
example_fs = """
|
||||
#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);
|
||||
}
|
||||
"""
|
||||
|
||||
# Validation pipeline
|
||||
print("Running shader pipeline validation...")
|
||||
|
||||
# 1. Extract attributes
|
||||
vs_inputs = validator.extract_shader_inputs(example_vs, "example_vs")
|
||||
vs_outputs = validator.extract_shader_outputs(example_vs, "example_vs")
|
||||
fs_inputs = validator.extract_shader_inputs(example_fs, "example_fs")
|
||||
|
||||
# 2. Validate vertex layout matching
|
||||
validator.validate_vertex_layout_match(vs_inputs, vertex_layout, "example_vs")
|
||||
|
||||
# 3. Validate stride
|
||||
validator.validate_vertex_buffer_stride(vertex_layout, vertex_struct_size)
|
||||
|
||||
# 4. Validate location continuity
|
||||
validator.validate_location_continuity(vs_inputs, "example_vs")
|
||||
|
||||
# 5. Validate SPIR-V compatibility
|
||||
validator.validate_spir_v_compatibility(example_vs, "example_vs")
|
||||
validator.validate_spir_v_compatibility(example_fs, "example_fs")
|
||||
|
||||
# 6. Validate interface matching
|
||||
validator.validate_interface_matching(vs_outputs, fs_inputs, "example_vs", "example_fs")
|
||||
|
||||
# 7. Validate naming conventions
|
||||
validator.validate_attribute_name_conventions(vs_inputs, "example_vs")
|
||||
|
||||
# Print report
|
||||
success = validator.print_report()
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -8,6 +8,7 @@ namespace sdl3cpp::core {
|
||||
struct Vertex {
|
||||
std::array<float, 3> position;
|
||||
std::array<float, 3> normal;
|
||||
std::array<float, 3> tangent;
|
||||
std::array<float, 2> texcoord;
|
||||
std::array<float, 3> color;
|
||||
};
|
||||
|
||||
@@ -285,10 +285,11 @@ BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr<IConfigService> configS
|
||||
", platformService=" + std::string(platformService_ ? "set" : "null"));
|
||||
}
|
||||
vertexLayout_.begin()
|
||||
.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) // location 0
|
||||
.add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) // location 1
|
||||
.add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) // location 2
|
||||
.add(bgfx::Attrib::Color0, 3, bgfx::AttribType::Float) // location 3
|
||||
.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
|
||||
.add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float)
|
||||
.add(bgfx::Attrib::Tangent, 3, bgfx::AttribType::Float)
|
||||
.add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
|
||||
.add(bgfx::Attrib::Color0, 3, bgfx::AttribType::Float)
|
||||
.end();
|
||||
|
||||
const std::array<float, 16> identity = {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "materialx_shader_generator.hpp"
|
||||
#include "shader_pipeline_validator.hpp"
|
||||
#include "../../core/vertex.hpp"
|
||||
|
||||
#include <MaterialXCore/Document.h>
|
||||
#include <MaterialXFormat/File.h>
|
||||
@@ -385,6 +387,95 @@ std::string ConvertIndividualOutputsToBlock(const std::string& source,
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string RemapVertexShaderInputLocations(const std::string& source,
|
||||
const std::shared_ptr<ILogger>& logger) {
|
||||
// Remap vertex shader input locations to match bgfx vertex layout order
|
||||
// WITHOUT creating an input block (which is not allowed in vertex shaders)
|
||||
|
||||
std::map<std::string, int> bgfxLocationMap;
|
||||
bgfxLocationMap["i_position"] = 0;
|
||||
bgfxLocationMap["i_normal"] = 1;
|
||||
bgfxLocationMap["i_tangent"] = 2;
|
||||
bgfxLocationMap["i_texcoord_0"] = 3;
|
||||
bgfxLocationMap["i_texcoord_1"] = 4;
|
||||
bgfxLocationMap["i_color0"] = 5;
|
||||
|
||||
std::string result = source;
|
||||
const std::string layoutToken = "layout (location =";
|
||||
const std::string layoutTokenCompact = "layout(location =";
|
||||
|
||||
// Find all input declarations and remap their locations
|
||||
size_t searchPos = 0;
|
||||
while (true) {
|
||||
size_t layoutPos = result.find(layoutToken, searchPos);
|
||||
size_t compactPos = result.find(layoutTokenCompact, searchPos);
|
||||
size_t tokenLength = 0;
|
||||
|
||||
if (compactPos != std::string::npos &&
|
||||
(layoutPos == std::string::npos || compactPos < layoutPos)) {
|
||||
layoutPos = compactPos;
|
||||
tokenLength = layoutTokenCompact.size();
|
||||
} else {
|
||||
tokenLength = layoutToken.size();
|
||||
}
|
||||
|
||||
if (layoutPos == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if this line contains " in " (vertex input)
|
||||
size_t lineEnd = result.find('\n', layoutPos);
|
||||
if (lineEnd == std::string::npos) lineEnd = result.size();
|
||||
std::string line = result.substr(layoutPos, lineEnd - layoutPos);
|
||||
|
||||
if (line.find(" in ") == std::string::npos) {
|
||||
searchPos = lineEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract variable name
|
||||
size_t inPos = line.find(" in ");
|
||||
size_t typeStart = inPos + 4;
|
||||
while (typeStart < line.size() && std::isspace(line[typeStart])) ++typeStart;
|
||||
size_t typeEnd = typeStart;
|
||||
while (typeEnd < line.size() && !std::isspace(line[typeEnd])) ++typeEnd;
|
||||
size_t nameStart = typeEnd;
|
||||
while (nameStart < line.size() && std::isspace(line[nameStart])) ++nameStart;
|
||||
size_t nameEnd = nameStart;
|
||||
while (nameEnd < line.size() && !std::isspace(line[nameEnd]) && line[nameEnd] != ';') ++nameEnd;
|
||||
std::string name = line.substr(nameStart, nameEnd - nameStart);
|
||||
|
||||
// Check if we need to remap this attribute
|
||||
if (bgfxLocationMap.count(name) > 0) {
|
||||
int newLoc = bgfxLocationMap[name];
|
||||
|
||||
// Find the old location number
|
||||
size_t locStart = layoutPos + tokenLength;
|
||||
while (locStart < result.size() && std::isspace(result[locStart])) ++locStart;
|
||||
size_t locEnd = locStart;
|
||||
while (locEnd < result.size() && std::isdigit(result[locEnd])) ++locEnd;
|
||||
|
||||
std::string oldLocStr = result.substr(locStart, locEnd - locStart);
|
||||
std::string newLocStr = std::to_string(newLoc);
|
||||
|
||||
// Replace the location number
|
||||
result.replace(locStart, locEnd - locStart, newLocStr);
|
||||
|
||||
if (logger) {
|
||||
logger->Trace("MaterialXShaderGenerator", "RemapVertexShaderInputLocations",
|
||||
"Remapped " + name + ": location " + oldLocStr + " -> " + newLocStr);
|
||||
}
|
||||
|
||||
// Adjust searchPos for the length change
|
||||
searchPos = locStart + newLocStr.size();
|
||||
} else {
|
||||
searchPos = lineEnd;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ConvertIndividualInputsToBlock(const std::string& source,
|
||||
const std::shared_ptr<ILogger>& logger) {
|
||||
// Find individual input declarations like:
|
||||
@@ -540,11 +631,39 @@ std::string ConvertIndividualInputsToBlock(const std::string& source,
|
||||
return std::get<0>(left) < std::get<0>(right);
|
||||
});
|
||||
|
||||
// Build the VertexData block with remapped locations.
|
||||
std::string block = "in VertexData\n{\n";
|
||||
// Build the VertexData block with locations matching bgfx vertex layout order.
|
||||
// bgfx assigns locations sequentially: Position=0, Normal=1, Tangent=2, TexCoord0=3
|
||||
std::map<std::string, int> bgfxLocationMap;
|
||||
bgfxLocationMap["i_position"] = 0;
|
||||
bgfxLocationMap["i_normal"] = 1;
|
||||
bgfxLocationMap["i_tangent"] = 2;
|
||||
bgfxLocationMap["i_texcoord_0"] = 3;
|
||||
bgfxLocationMap["i_texcoord_1"] = 4;
|
||||
bgfxLocationMap["i_color0"] = 5;
|
||||
|
||||
std::vector<std::tuple<int, std::string, std::string>> bgfxRemapped;
|
||||
for (const auto& [loc, type, name] : remapped) {
|
||||
int bgfxLoc = loc; // default to original
|
||||
if (bgfxLocationMap.count(name) > 0) {
|
||||
bgfxLoc = bgfxLocationMap[name];
|
||||
}
|
||||
bgfxRemapped.push_back({bgfxLoc, type, name});
|
||||
}
|
||||
|
||||
// Sort by bgfx location
|
||||
std::sort(bgfxRemapped.begin(), bgfxRemapped.end(),
|
||||
[](const auto& left, const auto& right) {
|
||||
return std::get<0>(left) < std::get<0>(right);
|
||||
});
|
||||
|
||||
std::string block = "in VertexData\n{\n";
|
||||
for (const auto& [loc, type, name] : bgfxRemapped) {
|
||||
block += " layout (location = " + std::to_string(loc) + ") " +
|
||||
type + " " + name + ";\n";
|
||||
if (logger) {
|
||||
logger->Trace("MaterialXShaderGenerator", "ConvertIndividualInputsToBlock",
|
||||
"Input " + name + " assigned to location " + std::to_string(loc));
|
||||
}
|
||||
}
|
||||
block += "} vd;\n\n";
|
||||
|
||||
@@ -882,16 +1001,20 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config,
|
||||
|
||||
// Log raw vertex shader inputs to debug Vulkan vertex attribute location mismatch
|
||||
if (logger_) {
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate", "RAW_VERTEX_SHADER:\n" +
|
||||
logger_->Trace("MaterialXShaderGenerator", "Generate", "RAW_VERTEX_SHADER:\n" +
|
||||
paths.vertexSource.substr(0, std::min(size_t(800), paths.vertexSource.size())));
|
||||
}
|
||||
|
||||
// Fix vertex shader inputs: remap locations to match bgfx vertex layout order
|
||||
// Note: We DON'T create an input block for vertex shaders (GLSL doesn't allow it)
|
||||
paths.vertexSource = RemapVertexShaderInputLocations(paths.vertexSource, logger_);
|
||||
|
||||
// Fix vertex shader outputs: convert individual layout outputs to VertexData block
|
||||
// MaterialX VkShaderGenerator incorrectly emits individual out variables instead of
|
||||
// a VertexData struct block, which causes compilation errors when the shader code
|
||||
// references vd.normalWorld etc. We convert them here as a workaround.
|
||||
paths.vertexSource = ConvertIndividualOutputsToBlock(paths.vertexSource, logger_);
|
||||
|
||||
|
||||
// Fix fragment shader inputs: convert individual layout inputs to VertexData block
|
||||
paths.fragmentSource = ConvertIndividualInputsToBlock(paths.fragmentSource, logger_);
|
||||
|
||||
@@ -935,6 +1058,42 @@ ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config,
|
||||
", fragmentVertexDataBlock=" + std::string(fragmentHasBlock ? "present" : "absent") +
|
||||
", vertexUsesVertexData=" + std::string(vertexUsesInstance ? "true" : "false"));
|
||||
}
|
||||
|
||||
// === MEGA-STRICT SHADER PIPELINE VALIDATION ===
|
||||
// Validate the shader pipeline BEFORE returning to prevent GPU driver crashes
|
||||
ShaderPipelineValidator validator(logger_);
|
||||
|
||||
// Define expected vertex layout (must match bgfx_graphics_backend.cpp)
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> expectedLayout = {
|
||||
ShaderPipelineValidator::AttributeInfo(0, "vec3", "Position", 12),
|
||||
ShaderPipelineValidator::AttributeInfo(1, "vec3", "Normal", 12),
|
||||
ShaderPipelineValidator::AttributeInfo(2, "vec3", "Tangent", 12),
|
||||
ShaderPipelineValidator::AttributeInfo(3, "vec2", "TexCoord0", 8),
|
||||
ShaderPipelineValidator::AttributeInfo(4, "vec3", "Color0", 12),
|
||||
};
|
||||
|
||||
// Validate the complete pipeline
|
||||
std::string pipelineName = config.documentPath.filename().string();
|
||||
auto result = validator.ValidatePipeline(
|
||||
paths.vertexSource,
|
||||
paths.fragmentSource,
|
||||
expectedLayout,
|
||||
sizeof(core::Vertex), // Actual vertex struct size
|
||||
pipelineName
|
||||
);
|
||||
|
||||
// Log the validation result
|
||||
validator.LogValidationResult(result, "MaterialX Pipeline: " + pipelineName);
|
||||
|
||||
// CRITICAL: If validation fails, throw an exception to prevent GPU driver crash
|
||||
if (!result.passed) {
|
||||
std::string errorMsg = "Shader pipeline validation failed for '" + pipelineName + "':\n";
|
||||
for (const auto& error : result.errors) {
|
||||
errorMsg += " - " + error + "\n";
|
||||
}
|
||||
throw std::runtime_error(errorMsg);
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ bool BuildPayloadFromBspBuffer(const std::vector<uint8_t>& buffer,
|
||||
|
||||
outPayload.positions.resize(vertexCount);
|
||||
outPayload.normals.resize(vertexCount);
|
||||
outPayload.tangents.resize(vertexCount);
|
||||
outPayload.colors.resize(vertexCount);
|
||||
outPayload.texcoords.resize(vertexCount);
|
||||
outPayload.indices.clear();
|
||||
@@ -293,6 +294,29 @@ bool BuildPayloadFromBspBuffer(const std::vector<uint8_t>& buffer,
|
||||
const BspVertex& vertex = vertices[i];
|
||||
outPayload.positions[i] = {vertex.position[0], vertex.position[1], vertex.position[2]};
|
||||
outPayload.normals[i] = {vertex.normal[0], vertex.normal[1], vertex.normal[2]};
|
||||
|
||||
// Compute a simple tangent perpendicular to the normal
|
||||
float nx = vertex.normal[0];
|
||||
float ny = vertex.normal[1];
|
||||
float nz = vertex.normal[2];
|
||||
// Find a vector not parallel to the normal
|
||||
float tx, ty, tz;
|
||||
if (std::fabs(nx) < 0.9f) {
|
||||
tx = 1.0f; ty = 0.0f; tz = 0.0f;
|
||||
} else {
|
||||
tx = 0.0f; ty = 1.0f; tz = 0.0f;
|
||||
}
|
||||
// Cross product: tangent = arbitrary × normal
|
||||
float cx = ty * nz - tz * ny;
|
||||
float cy = tz * nx - tx * nz;
|
||||
float cz = tx * ny - ty * nx;
|
||||
// Normalize
|
||||
float len = std::sqrt(cx*cx + cy*cy + cz*cz);
|
||||
if (len > 0.0001f) {
|
||||
cx /= len; cy /= len; cz /= len;
|
||||
}
|
||||
outPayload.tangents[i] = {cx, cy, cz};
|
||||
|
||||
outPayload.colors[i] = {
|
||||
static_cast<float>(vertex.color[0]) / 255.0f,
|
||||
static_cast<float>(vertex.color[1]) / 255.0f,
|
||||
@@ -491,12 +515,14 @@ bool AppendMeshPayload(const aiScene* scene,
|
||||
|
||||
size_t positionsStart = outPayload.positions.size();
|
||||
size_t normalsStart = outPayload.normals.size();
|
||||
size_t tangentsStart = outPayload.tangents.size();
|
||||
size_t colorsStart = outPayload.colors.size();
|
||||
size_t texcoordsStart = outPayload.texcoords.size();
|
||||
size_t indicesStart = outPayload.indices.size();
|
||||
|
||||
outPayload.positions.reserve(positionsStart + mesh->mNumVertices);
|
||||
outPayload.normals.reserve(normalsStart + mesh->mNumVertices);
|
||||
outPayload.tangents.reserve(tangentsStart + mesh->mNumVertices);
|
||||
outPayload.colors.reserve(colorsStart + mesh->mNumVertices);
|
||||
outPayload.texcoords.reserve(texcoordsStart + mesh->mNumVertices);
|
||||
outPayload.indices.reserve(indicesStart + mesh->mNumFaces * 3);
|
||||
@@ -511,6 +537,21 @@ bool AppendMeshPayload(const aiScene* scene,
|
||||
}
|
||||
outPayload.normals.push_back({normal.x, normal.y, normal.z});
|
||||
|
||||
// Compute tangent
|
||||
aiVector3D tangent(1.0f, 0.0f, 0.0f);
|
||||
if (mesh->HasTangentsAndBitangents()) {
|
||||
tangent = mesh->mTangents[i];
|
||||
} else {
|
||||
// Generate a tangent perpendicular to the normal
|
||||
aiVector3D arbitrary(1.0f, 0.0f, 0.0f);
|
||||
if (std::fabs(normal.x) > 0.9f) {
|
||||
arbitrary = aiVector3D(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
tangent = (arbitrary ^ normal); // Cross product
|
||||
tangent.Normalize();
|
||||
}
|
||||
outPayload.tangents.push_back({tangent.x, tangent.y, tangent.z});
|
||||
|
||||
aiColor3D color = materialColor;
|
||||
if (mesh->HasVertexColors(0) && mesh->mColors[0]) {
|
||||
const aiColor4D& vertexColor = mesh->mColors[0][i];
|
||||
@@ -543,6 +584,7 @@ bool AppendMeshPayload(const aiScene* scene,
|
||||
if (outIndicesAdded == 0) {
|
||||
outPayload.positions.resize(positionsStart);
|
||||
outPayload.normals.resize(normalsStart);
|
||||
outPayload.tangents.resize(tangentsStart);
|
||||
outPayload.colors.resize(colorsStart);
|
||||
outPayload.indices.resize(indicesStart);
|
||||
outError = "Mesh contains no triangle faces";
|
||||
@@ -872,6 +914,17 @@ void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) {
|
||||
}
|
||||
lua_setfield(L, -2, "normal");
|
||||
|
||||
lua_newtable(L);
|
||||
std::array<float, 3> tangent = {1.0f, 0.0f, 0.0f};
|
||||
if (vertexIndex < payload.tangents.size()) {
|
||||
tangent = payload.tangents[vertexIndex];
|
||||
}
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
lua_pushnumber(L, tangent[component]);
|
||||
lua_rawseti(L, -2, component + 1);
|
||||
}
|
||||
lua_setfield(L, -2, "tangent");
|
||||
|
||||
lua_newtable(L);
|
||||
for (int component = 0; component < 3; ++component) {
|
||||
lua_pushnumber(L, payload.colors[vertexIndex][component]);
|
||||
|
||||
@@ -95,6 +95,14 @@ std::vector<core::Vertex> ReadVertexArray(lua_State* L, int index, const std::sh
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, vertexIndex, "tangent");
|
||||
if (lua_istable(L, -1)) {
|
||||
vertex.tangent = lua::ReadVector3(L, -1);
|
||||
} else {
|
||||
vertex.tangent = {1.0f, 0.0f, 0.0f};
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, vertexIndex, "color");
|
||||
vertex.color = lua::ReadVector3(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
280
src/services/impl/shader_pipeline_validator.cpp
Normal file
280
src/services/impl/shader_pipeline_validator.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
#include "shader_pipeline_validator.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include <sstream>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo>
|
||||
ShaderPipelineValidator::ExtractShaderInputs(const std::string& glslSource) const {
|
||||
std::vector<AttributeInfo> inputs;
|
||||
|
||||
// Match: layout (location = N) in type name;
|
||||
std::regex pattern(R"(layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s+in\s+(\w+)\s+(\w+)\s*;)");
|
||||
|
||||
std::sregex_iterator begin(glslSource.begin(), glslSource.end(), pattern);
|
||||
std::sregex_iterator end;
|
||||
|
||||
for (auto it = begin; it != end; ++it) {
|
||||
int location = std::stoi((*it)[1].str());
|
||||
std::string type = (*it)[2].str();
|
||||
std::string name = (*it)[3].str();
|
||||
size_t size = GetGlslTypeSize(type);
|
||||
|
||||
inputs.emplace_back(location, type, name, size);
|
||||
|
||||
// Trace logging disabled to avoid API mismatch
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo>
|
||||
ShaderPipelineValidator::ExtractShaderOutputs(const std::string& glslSource) const {
|
||||
std::vector<AttributeInfo> outputs;
|
||||
|
||||
// Match: layout (location = N) out type name;
|
||||
std::regex pattern(R"(layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s+out\s+(\w+)\s+(\w+)\s*;)");
|
||||
|
||||
std::sregex_iterator begin(glslSource.begin(), glslSource.end(), pattern);
|
||||
std::sregex_iterator end;
|
||||
|
||||
for (auto it = begin; it != end; ++it) {
|
||||
int location = std::stoi((*it)[1].str());
|
||||
std::string type = (*it)[2].str();
|
||||
std::string name = (*it)[3].str();
|
||||
size_t size = GetGlslTypeSize(type);
|
||||
|
||||
outputs.emplace_back(location, type, name, size);
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
ShaderPipelineValidator::ValidationResult
|
||||
ShaderPipelineValidator::ValidateVertexLayoutMatch(
|
||||
const std::vector<AttributeInfo>& shaderInputs,
|
||||
const std::vector<AttributeInfo>& vertexLayoutAttribs,
|
||||
const std::string& shaderName) const {
|
||||
|
||||
ValidationResult result;
|
||||
|
||||
// Build maps for quick lookup
|
||||
std::map<int, const AttributeInfo*> shaderMap;
|
||||
std::map<int, const AttributeInfo*> layoutMap;
|
||||
|
||||
for (const auto& attr : shaderInputs) {
|
||||
shaderMap[attr.location] = &attr;
|
||||
}
|
||||
for (const auto& attr : vertexLayoutAttribs) {
|
||||
layoutMap[attr.location] = &attr;
|
||||
}
|
||||
|
||||
// Check that all shader inputs are satisfied by layout
|
||||
for (const auto& [loc, shaderAttr] : shaderMap) {
|
||||
auto layoutIt = layoutMap.find(loc);
|
||||
if (layoutIt == layoutMap.end()) {
|
||||
result.AddError("Shader '" + shaderName + "' expects input at location " +
|
||||
std::to_string(loc) + " (" + shaderAttr->name + ": " +
|
||||
shaderAttr->type + ") but vertex layout doesn't provide it");
|
||||
} else {
|
||||
const AttributeInfo* layoutAttr = layoutIt->second;
|
||||
|
||||
// Type compatibility check
|
||||
if (!TypesCompatible(shaderAttr->type, layoutAttr->type)) {
|
||||
result.AddError("Type mismatch at location " + std::to_string(loc) +
|
||||
": shader expects " + shaderAttr->type +
|
||||
" but vertex layout provides " + layoutAttr->type);
|
||||
}
|
||||
|
||||
// Size check
|
||||
if (shaderAttr->sizeBytes != layoutAttr->sizeBytes) {
|
||||
result.AddWarning("Size mismatch at location " + std::to_string(loc) +
|
||||
": shader expects " + std::to_string(shaderAttr->sizeBytes) +
|
||||
" bytes but layout provides " + std::to_string(layoutAttr->sizeBytes) +
|
||||
" bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warn about unused layout attributes
|
||||
for (const auto& [loc, layoutAttr] : layoutMap) {
|
||||
if (shaderMap.find(loc) == shaderMap.end()) {
|
||||
result.AddWarning("Vertex layout provides unused attribute at location " +
|
||||
std::to_string(loc) + " (" + layoutAttr->name + ")");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ShaderPipelineValidator::ValidationResult
|
||||
ShaderPipelineValidator::ValidateVertexStride(
|
||||
const std::vector<AttributeInfo>& layoutAttribs,
|
||||
size_t actualVertexStructSize) const {
|
||||
|
||||
ValidationResult result;
|
||||
|
||||
// Calculate expected stride from layout
|
||||
size_t expectedStride = 0;
|
||||
for (const auto& attr : layoutAttribs) {
|
||||
expectedStride += attr.sizeBytes;
|
||||
}
|
||||
|
||||
if (expectedStride != actualVertexStructSize) {
|
||||
std::ostringstream oss;
|
||||
oss << "Vertex layout stride mismatch: layout expects " << expectedStride
|
||||
<< " bytes but actual vertex struct is " << actualVertexStructSize << " bytes.\n";
|
||||
oss << "Layout breakdown:\n";
|
||||
for (const auto& attr : layoutAttribs) {
|
||||
oss << " " << attr.name << " (" << attr.type << "): " << attr.sizeBytes << " bytes\n";
|
||||
}
|
||||
result.AddError(oss.str());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ShaderPipelineValidator::ValidationResult
|
||||
ShaderPipelineValidator::ValidateInterfaceMatching(
|
||||
const std::vector<AttributeInfo>& vsOutputs,
|
||||
const std::vector<AttributeInfo>& fsInputs,
|
||||
const std::string& vsName,
|
||||
const std::string& fsName) const {
|
||||
|
||||
ValidationResult result;
|
||||
|
||||
std::map<int, const AttributeInfo*> vsMap;
|
||||
std::map<int, const AttributeInfo*> fsMap;
|
||||
|
||||
for (const auto& attr : vsOutputs) {
|
||||
vsMap[attr.location] = &attr;
|
||||
}
|
||||
for (const auto& attr : fsInputs) {
|
||||
fsMap[attr.location] = &attr;
|
||||
}
|
||||
|
||||
// Fragment shader inputs must be provided by vertex shader outputs
|
||||
for (const auto& [loc, fsAttr] : fsMap) {
|
||||
auto vsIt = vsMap.find(loc);
|
||||
if (vsIt == vsMap.end()) {
|
||||
result.AddError("Fragment shader '" + fsName + "' expects input at location " +
|
||||
std::to_string(loc) + " (" + fsAttr->name + ") but vertex shader '" +
|
||||
vsName + "' doesn't output it");
|
||||
} else {
|
||||
const AttributeInfo* vsAttr = vsIt->second;
|
||||
if (vsAttr->type != fsAttr->type) {
|
||||
result.AddError("Type mismatch at location " + std::to_string(loc) +
|
||||
": VS outputs " + vsAttr->type + " but FS expects " + fsAttr->type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ShaderPipelineValidator::ValidationResult
|
||||
ShaderPipelineValidator::ValidateSpirvRequirements(
|
||||
const std::string& glslSource,
|
||||
const std::string& shaderName) const {
|
||||
|
||||
ValidationResult result;
|
||||
|
||||
// Check that all in/out variables have layout(location=N)
|
||||
std::regex inOutPattern(R"(\b(in|out)\s+\w+\s+\w+\s*;)");
|
||||
std::regex layoutPattern(R"(layout\s*\()");
|
||||
|
||||
size_t lineNum = 1;
|
||||
std::istringstream stream(glslSource);
|
||||
std::string line;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
// Skip comments and version directives
|
||||
if (line.find("//") != std::string::npos ||
|
||||
line.find("#version") != std::string::npos ||
|
||||
line.find("gl_") != std::string::npos) {
|
||||
lineNum++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for in/out without layout
|
||||
std::smatch inOutMatch;
|
||||
if (std::regex_search(line, inOutMatch, inOutPattern)) {
|
||||
if (!std::regex_search(line, layoutPattern)) {
|
||||
result.AddError(std::string("SPIR-V requires 'layout(location=N)' for all inputs/outputs ") +
|
||||
"at line " + std::to_string(lineNum) + " in shader '" + shaderName + "'");
|
||||
}
|
||||
}
|
||||
|
||||
lineNum++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ShaderPipelineValidator::ValidationResult
|
||||
ShaderPipelineValidator::ValidatePipeline(
|
||||
const std::string& vertexShaderSource,
|
||||
const std::string& fragmentShaderSource,
|
||||
const std::vector<AttributeInfo>& vertexLayoutAttribs,
|
||||
size_t actualVertexStructSize,
|
||||
const std::string& pipelineName) const {
|
||||
|
||||
ValidationResult combined;
|
||||
|
||||
// Extract attributes
|
||||
auto vsInputs = ExtractShaderInputs(vertexShaderSource);
|
||||
auto vsOutputs = ExtractShaderOutputs(vertexShaderSource);
|
||||
auto fsInputs = ExtractShaderInputs(fragmentShaderSource);
|
||||
|
||||
// Run all validations
|
||||
auto layoutMatch = ValidateVertexLayoutMatch(vsInputs, vertexLayoutAttribs,
|
||||
pipelineName + ":vertex");
|
||||
auto strideCheck = ValidateVertexStride(vertexLayoutAttribs, actualVertexStructSize);
|
||||
auto interfaceMatch = ValidateInterfaceMatching(vsOutputs, fsInputs,
|
||||
pipelineName + ":vertex",
|
||||
pipelineName + ":fragment");
|
||||
auto spirvVs = ValidateSpirvRequirements(vertexShaderSource, pipelineName + ":vertex");
|
||||
auto spirvFs = ValidateSpirvRequirements(fragmentShaderSource, pipelineName + ":fragment");
|
||||
|
||||
// Combine results
|
||||
auto mergeResults = [&](const ValidationResult& r) {
|
||||
if (!r.passed) combined.passed = false;
|
||||
combined.errors.insert(combined.errors.end(), r.errors.begin(), r.errors.end());
|
||||
combined.warnings.insert(combined.warnings.end(), r.warnings.begin(), r.warnings.end());
|
||||
};
|
||||
|
||||
mergeResults(layoutMatch);
|
||||
mergeResults(strideCheck);
|
||||
mergeResults(interfaceMatch);
|
||||
mergeResults(spirvVs);
|
||||
mergeResults(spirvFs);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
void ShaderPipelineValidator::LogValidationResult(
|
||||
const ValidationResult& result,
|
||||
const std::string& context) const {
|
||||
|
||||
if (!logger_) return;
|
||||
|
||||
if (result.passed && result.warnings.empty()) {
|
||||
logger_->Info("[" + context + "] ✓ Validation passed");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& error : result.errors) {
|
||||
logger_->Error("[" + context + "] ✗ " + error);
|
||||
}
|
||||
|
||||
for (const auto& warning : result.warnings) {
|
||||
logger_->Warn("[" + context + "] ⚠ " + warning);
|
||||
}
|
||||
|
||||
if (!result.passed) {
|
||||
logger_->Error("[" + context + "] ❌ Validation FAILED with " +
|
||||
std::to_string(result.errors.size()) + " errors");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
130
src/services/impl/shader_pipeline_validator.hpp
Normal file
130
src/services/impl/shader_pipeline_validator.hpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
// Forward declarations
|
||||
class ILogger;
|
||||
|
||||
/**
|
||||
* Compile-time and runtime shader pipeline validator
|
||||
* Catches mismatches between shaders, vertex layouts, and vertex data
|
||||
* BEFORE submitting to the GPU driver
|
||||
*/
|
||||
class ShaderPipelineValidator {
|
||||
public:
|
||||
struct AttributeInfo {
|
||||
int location;
|
||||
std::string type; // "vec3", "vec2", etc.
|
||||
std::string name;
|
||||
size_t sizeBytes; // Expected size in bytes
|
||||
|
||||
AttributeInfo(int loc, const std::string& t, const std::string& n, size_t sz)
|
||||
: location(loc), type(t), name(n), sizeBytes(sz) {}
|
||||
};
|
||||
|
||||
struct ValidationResult {
|
||||
bool passed;
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
|
||||
void AddError(const std::string& msg) {
|
||||
errors.push_back(msg);
|
||||
passed = false;
|
||||
}
|
||||
|
||||
void AddWarning(const std::string& msg) {
|
||||
warnings.push_back(msg);
|
||||
}
|
||||
|
||||
ValidationResult() : passed(true) {}
|
||||
};
|
||||
|
||||
explicit ShaderPipelineValidator(std::shared_ptr<ILogger> logger = nullptr)
|
||||
: logger_(std::move(logger)) {}
|
||||
|
||||
// Extract attribute information from GLSL source
|
||||
std::vector<AttributeInfo> ExtractShaderInputs(const std::string& glslSource) const;
|
||||
std::vector<AttributeInfo> ExtractShaderOutputs(const std::string& glslSource) const;
|
||||
|
||||
// Validate that vertex layout matches shader expectations
|
||||
ValidationResult ValidateVertexLayoutMatch(
|
||||
const std::vector<AttributeInfo>& shaderInputs,
|
||||
const std::vector<AttributeInfo>& vertexLayoutAttribs,
|
||||
const std::string& shaderName) const;
|
||||
|
||||
// Validate that vertex struct size matches layout stride
|
||||
ValidationResult ValidateVertexStride(
|
||||
const std::vector<AttributeInfo>& layoutAttribs,
|
||||
size_t actualVertexStructSize) const;
|
||||
|
||||
// Validate that vertex shader outputs match fragment shader inputs
|
||||
ValidationResult ValidateInterfaceMatching(
|
||||
const std::vector<AttributeInfo>& vsOutputs,
|
||||
const std::vector<AttributeInfo>& fsInputs,
|
||||
const std::string& vsName,
|
||||
const std::string& fsName) const;
|
||||
|
||||
// Validate SPIR-V requirements
|
||||
ValidationResult ValidateSpirvRequirements(
|
||||
const std::string& glslSource,
|
||||
const std::string& shaderName) const;
|
||||
|
||||
// Comprehensive validation - runs all checks
|
||||
ValidationResult ValidatePipeline(
|
||||
const std::string& vertexShaderSource,
|
||||
const std::string& fragmentShaderSource,
|
||||
const std::vector<AttributeInfo>& vertexLayoutAttribs,
|
||||
size_t actualVertexStructSize,
|
||||
const std::string& pipelineName) const;
|
||||
|
||||
// Log validation result
|
||||
void LogValidationResult(const ValidationResult& result, const std::string& context) const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
|
||||
// Helper to get expected size for a GLSL type
|
||||
size_t GetGlslTypeSize(const std::string& glslType) const {
|
||||
static const std::map<std::string, size_t> sizes = {
|
||||
{"float", 4},
|
||||
{"vec2", 8},
|
||||
{"vec3", 12},
|
||||
{"vec4", 16},
|
||||
{"int", 4},
|
||||
{"ivec2", 8},
|
||||
{"ivec3", 12},
|
||||
{"ivec4", 16},
|
||||
{"uint", 4},
|
||||
{"uvec2", 8},
|
||||
{"uvec3", 12},
|
||||
{"uvec4", 16},
|
||||
};
|
||||
|
||||
auto it = sizes.find(glslType);
|
||||
return it != sizes.end() ? it->second : 0;
|
||||
}
|
||||
|
||||
bool TypesCompatible(const std::string& shaderType, const std::string& layoutType) const {
|
||||
// Add compatibility mappings
|
||||
if (shaderType == layoutType) return true;
|
||||
|
||||
// vec3 <-> float3 etc
|
||||
std::map<std::string, std::string> aliases = {
|
||||
{"vec2", "float2"}, {"vec3", "float3"}, {"vec4", "float4"},
|
||||
{"ivec2", "int2"}, {"ivec3", "int3"}, {"ivec4", "int4"},
|
||||
};
|
||||
|
||||
auto it = aliases.find(shaderType);
|
||||
return it != aliases.end() && it->second == layoutType;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
@@ -9,6 +9,7 @@ namespace sdl3cpp::services {
|
||||
struct MeshPayload {
|
||||
std::vector<std::array<float, 3>> positions;
|
||||
std::vector<std::array<float, 3>> normals;
|
||||
std::vector<std::array<float, 3>> tangents;
|
||||
std::vector<std::array<float, 3>> colors;
|
||||
std::vector<std::array<float, 2>> texcoords;
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
537
tests/shader_pipeline_validator_test.cpp
Normal file
537
tests/shader_pipeline_validator_test.cpp
Normal file
@@ -0,0 +1,537 @@
|
||||
#include "services/impl/shader_pipeline_validator.hpp"
|
||||
#include "core/vertex.hpp"
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
|
||||
using namespace sdl3cpp::services;
|
||||
|
||||
// Mock logger for testing
|
||||
class MockLogger : public ILogger {
|
||||
public:
|
||||
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 Trace(const std::string&) override {}
|
||||
};
|
||||
|
||||
int TestExtractShaderInputs() {
|
||||
std::cout << "TEST: ExtractShaderInputs\n";
|
||||
int failures = 0;
|
||||
|
||||
auto logger = std::make_shared<MockLogger>();
|
||||
ShaderPipelineValidator validator(logger);
|
||||
|
||||
std::string validGlsl = R"(
|
||||
#version 450
|
||||
layout (location = 0) in vec3 i_position;
|
||||
layout (location = 1) in vec3 i_normal;
|
||||
layout (location = 2) in vec2 i_texcoord_0;
|
||||
void main() {}
|
||||
)";
|
||||
|
||||
auto inputs = validator.ExtractShaderInputs(validGlsl);
|
||||
|
||||
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<ShaderPipelineValidator::AttributeInfo> shaderInputs = {
|
||||
{0, "vec3", "i_position", 12},
|
||||
{1, "vec3", "i_normal", 12},
|
||||
{2, "vec2", "i_texcoord", 8},
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> shaderInputs = {
|
||||
{0, "vec3", "i_position", 12},
|
||||
{1, "vec3", "i_normal", 12},
|
||||
{2, "vec3", "i_tangent", 12}, // Shader expects tangent
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> shaderInputs = {
|
||||
{0, "vec3", "i_position", 12},
|
||||
{1, "vec4", "i_normal", 16}, // Expects vec4
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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_UnusedAttribute) {
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> shaderInputs = {
|
||||
{0, "vec3", "i_position", 12},
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> vsOutputs = {
|
||||
{0, "vec3", "v_normal", 12},
|
||||
{1, "vec2", "v_texcoord", 8},
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> vsOutputs = {
|
||||
{0, "vec3", "v_normal", 12},
|
||||
// Missing location 1!
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> vsOutputs = {
|
||||
{0, "vec3", "v_data", 12},
|
||||
};
|
||||
|
||||
std::vector<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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<ShaderPipelineValidator::AttributeInfo> 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();
|
||||
}
|
||||
Reference in New Issue
Block a user