validate_shader_uniforms.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #!/usr/bin/env python3
  2. """
  3. Shader Uniform Validation Tool
  4. This script validates that uniform names in GLSL shaders match the uniform
  5. names used in the C++ backend code. It helps prevent rendering bugs caused
  6. by naming convention mismatches (camelCase vs snake_case).
  7. Usage:
  8. python3 scripts/validate_shader_uniforms.py [--fix]
  9. Options:
  10. --fix Attempt to automatically fix naming mismatches in backend.cpp
  11. """
  12. import re
  13. import sys
  14. from pathlib import Path
  15. from typing import Dict, List, Set, Tuple
  16. from collections import defaultdict
  17. # ANSI color codes
  18. class Colors:
  19. RED = '\033[0;31m'
  20. GREEN = '\033[0;32m'
  21. YELLOW = '\033[1;33m'
  22. BLUE = '\033[0;34m'
  23. NC = '\033[0m' # No Color
  24. BOLD = '\033[1m'
  25. def find_shader_uniforms(shader_path: Path) -> Set[str]:
  26. """Extract all uniform variable names from a GLSL shader file."""
  27. uniforms = set()
  28. with open(shader_path, 'r') as f:
  29. content = f.read()
  30. # Match patterns like: uniform mat4 u_viewProj;
  31. # This regex captures the variable name after the type
  32. pattern = r'uniform\s+\w+\s+(\w+)\s*;'
  33. matches = re.findall(pattern, content)
  34. uniforms.update(matches)
  35. return uniforms
  36. def find_backend_uniform_calls(backend_path: Path) -> Dict[str, List[Tuple[int, str]]]:
  37. """
  38. Find all uniformHandle() calls in backend.cpp
  39. Returns dict mapping uniform names to list of (line_number, line_content) tuples
  40. """
  41. uniform_calls = defaultdict(list)
  42. with open(backend_path, 'r') as f:
  43. lines = f.readlines()
  44. # Match patterns like: uniformHandle("u_viewProj")
  45. pattern = r'uniformHandle\s*\(\s*"([^"]+)"\s*\)'
  46. for line_num, line in enumerate(lines, start=1):
  47. matches = re.findall(pattern, line)
  48. for uniform_name in matches:
  49. uniform_calls[uniform_name].append((line_num, line.strip()))
  50. return uniform_calls
  51. def convert_to_snake_case(name: str) -> str:
  52. """Convert camelCase to snake_case."""
  53. # Handle already snake_case names
  54. if '_' in name and not any(c.isupper() for c in name):
  55. return name
  56. # Insert underscore before uppercase letters
  57. s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
  58. return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
  59. def find_naming_mismatches(shader_uniforms: Dict[str, Set[str]],
  60. backend_uniforms: Dict[str, List[Tuple[int, str]]]) -> List[Dict]:
  61. """
  62. Find uniforms where backend uses different naming than shader.
  63. Returns list of mismatch dictionaries.
  64. """
  65. mismatches = []
  66. # Build reverse index: what shaders use each uniform
  67. uniform_to_shaders = defaultdict(list)
  68. for shader, uniforms in shader_uniforms.items():
  69. for uniform in uniforms:
  70. uniform_to_shaders[uniform].append(shader)
  71. # Check each backend uniform call
  72. for backend_name, locations in backend_uniforms.items():
  73. # Check if this exact name exists in any shader
  74. if backend_name not in uniform_to_shaders:
  75. # Try to find similar names in shaders (potential mismatch)
  76. for shader_name in uniform_to_shaders.keys():
  77. # Check if one is snake_case version of the other
  78. if (convert_to_snake_case(shader_name) == backend_name or
  79. convert_to_snake_case(backend_name) == shader_name or
  80. backend_name.replace('_', '') == shader_name.replace('_', '')):
  81. mismatches.append({
  82. 'backend_name': backend_name,
  83. 'shader_name': shader_name,
  84. 'shaders': uniform_to_shaders[shader_name],
  85. 'locations': locations
  86. })
  87. break
  88. return mismatches
  89. def main():
  90. project_root = Path(__file__).parent.parent
  91. shader_dir = project_root / "assets" / "shaders"
  92. backend_file = project_root / "render" / "gl" / "backend.cpp"
  93. if not shader_dir.exists():
  94. print(f"{Colors.RED}Error: Shader directory not found: {shader_dir}{Colors.NC}")
  95. return 1
  96. if not backend_file.exists():
  97. print(f"{Colors.RED}Error: Backend file not found: {backend_file}{Colors.NC}")
  98. return 1
  99. print(f"{Colors.BOLD}=== Shader Uniform Validation ==={Colors.NC}")
  100. print(f"Project root: {project_root}")
  101. print(f"Shader directory: {shader_dir}")
  102. print(f"Backend file: {backend_file}")
  103. print()
  104. # Find all shader files
  105. shader_files = list(shader_dir.glob("*.frag")) + list(shader_dir.glob("*.vert"))
  106. print(f"Found {len(shader_files)} shader files")
  107. # Extract uniforms from all shaders
  108. shader_uniforms = {}
  109. all_shader_uniform_names = set()
  110. for shader_path in shader_files:
  111. uniforms = find_shader_uniforms(shader_path)
  112. if uniforms:
  113. shader_uniforms[shader_path.name] = uniforms
  114. all_shader_uniform_names.update(uniforms)
  115. print(f" {shader_path.name}: {len(uniforms)} uniforms")
  116. print(f"\nTotal unique uniforms in shaders: {len(all_shader_uniform_names)}")
  117. # Extract uniform calls from backend
  118. backend_uniforms = find_backend_uniform_calls(backend_file)
  119. print(f"Found {len(backend_uniforms)} unique uniformHandle() calls in backend.cpp")
  120. print()
  121. # Find mismatches
  122. mismatches = find_naming_mismatches(shader_uniforms, backend_uniforms)
  123. if not mismatches:
  124. print(f"{Colors.GREEN}✓ All uniform names match between shaders and backend!{Colors.NC}")
  125. return 0
  126. # Report mismatches
  127. print(f"{Colors.RED}Found {len(mismatches)} naming mismatches:{Colors.NC}\n")
  128. for i, mismatch in enumerate(mismatches, 1):
  129. print(f"{Colors.BOLD}Mismatch #{i}:{Colors.NC}")
  130. print(f" Backend uses: {Colors.YELLOW}\"{mismatch['backend_name']}\"{Colors.NC}")
  131. print(f" Shader has: {Colors.GREEN}\"{mismatch['shader_name']}\"{Colors.NC}")
  132. print(f" Affected shaders: {', '.join(mismatch['shaders'])}")
  133. print(f" Locations in backend.cpp:")
  134. for line_num, line in mismatch['locations'][:3]: # Show first 3 locations
  135. print(f" Line {line_num}: {line}")
  136. if len(mismatch['locations']) > 3:
  137. print(f" ... and {len(mismatch['locations']) - 3} more")
  138. print()
  139. # Summary
  140. print(f"{Colors.BOLD}=== Summary ==={Colors.NC}")
  141. print(f" {Colors.RED}Errors: {len(mismatches)}{Colors.NC}")
  142. print()
  143. print(f"{Colors.YELLOW}These mismatches will cause uniforms to not be found at runtime,")
  144. print(f"resulting in rendering errors.{Colors.NC}")
  145. print()
  146. print(f"To fix: Update backend.cpp to use the exact uniform names from the shaders.")
  147. return 1 if mismatches else 0
  148. if __name__ == "__main__":
  149. sys.exit(main())