validate_shader_uniforms.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. class Colors:
  18. RED = "\033[0;31m"
  19. GREEN = "\033[0;32m"
  20. YELLOW = "\033[1;33m"
  21. BLUE = "\033[0;34m"
  22. NC = "\033[0m"
  23. BOLD = "\033[1m"
  24. def find_shader_uniforms(shader_path: Path) -> Set[str]:
  25. """Extract all uniform variable names from a GLSL shader file."""
  26. uniforms = set()
  27. with open(shader_path, "r") as f:
  28. content = f.read()
  29. pattern = r"uniform\s+\w+\s+(\w+)\s*;"
  30. matches = re.findall(pattern, content)
  31. uniforms.update(matches)
  32. return uniforms
  33. def find_backend_uniform_calls(backend_path: Path) -> Dict[str, List[Tuple[int, str]]]:
  34. """
  35. Find all uniformHandle() calls in backend.cpp
  36. Returns dict mapping uniform names to list of (line_number, line_content) tuples
  37. """
  38. uniform_calls = defaultdict(list)
  39. with open(backend_path, "r") as f:
  40. lines = f.readlines()
  41. pattern = r'uniformHandle\s*\(\s*"([^"]+)"\s*\)'
  42. for line_num, line in enumerate(lines, start=1):
  43. matches = re.findall(pattern, line)
  44. for uniform_name in matches:
  45. uniform_calls[uniform_name].append((line_num, line.strip()))
  46. return uniform_calls
  47. def convert_to_snake_case(name: str) -> str:
  48. """Convert camelCase to snake_case."""
  49. if "_" in name and not any(c.isupper() for c in name):
  50. return name
  51. s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
  52. return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
  53. def find_naming_mismatches(
  54. shader_uniforms: Dict[str, Set[str]],
  55. backend_uniforms: Dict[str, List[Tuple[int, str]]],
  56. ) -> List[Dict]:
  57. """
  58. Find uniforms where backend uses different naming than shader.
  59. Returns list of mismatch dictionaries.
  60. """
  61. mismatches = []
  62. uniform_to_shaders = defaultdict(list)
  63. for shader, uniforms in shader_uniforms.items():
  64. for uniform in uniforms:
  65. uniform_to_shaders[uniform].append(shader)
  66. for backend_name, locations in backend_uniforms.items():
  67. if backend_name not in uniform_to_shaders:
  68. for shader_name in uniform_to_shaders.keys():
  69. if (
  70. convert_to_snake_case(shader_name) == backend_name
  71. or convert_to_snake_case(backend_name) == shader_name
  72. or backend_name.replace("_", "") == shader_name.replace("_", "")
  73. ):
  74. mismatches.append(
  75. {
  76. "backend_name": backend_name,
  77. "shader_name": shader_name,
  78. "shaders": uniform_to_shaders[shader_name],
  79. "locations": locations,
  80. }
  81. )
  82. break
  83. return mismatches
  84. def main():
  85. project_root = Path(__file__).parent.parent
  86. shader_dir = project_root / "assets" / "shaders"
  87. backend_file = project_root / "render" / "gl" / "backend.cpp"
  88. if not shader_dir.exists():
  89. print(f"{Colors.RED}Error: Shader directory not found: {shader_dir}{Colors.NC}")
  90. return 1
  91. if not backend_file.exists():
  92. print(f"{Colors.RED}Error: Backend file not found: {backend_file}{Colors.NC}")
  93. return 1
  94. print(f"{Colors.BOLD}=== Shader Uniform Validation ==={Colors.NC}")
  95. print(f"Project root: {project_root}")
  96. print(f"Shader directory: {shader_dir}")
  97. print(f"Backend file: {backend_file}")
  98. print()
  99. shader_files = list(shader_dir.glob("*.frag")) + list(shader_dir.glob("*.vert"))
  100. print(f"Found {len(shader_files)} shader files")
  101. shader_uniforms = {}
  102. all_shader_uniform_names = set()
  103. for shader_path in shader_files:
  104. uniforms = find_shader_uniforms(shader_path)
  105. if uniforms:
  106. shader_uniforms[shader_path.name] = uniforms
  107. all_shader_uniform_names.update(uniforms)
  108. print(f" {shader_path.name}: {len(uniforms)} uniforms")
  109. print(f"\nTotal unique uniforms in shaders: {len(all_shader_uniform_names)}")
  110. backend_uniforms = find_backend_uniform_calls(backend_file)
  111. print(f"Found {len(backend_uniforms)} unique uniformHandle() calls in backend.cpp")
  112. print()
  113. mismatches = find_naming_mismatches(shader_uniforms, backend_uniforms)
  114. if not mismatches:
  115. print(
  116. f"{Colors.GREEN}✓ All uniform names match between shaders and backend!{Colors.NC}"
  117. )
  118. return 0
  119. print(f"{Colors.RED}Found {len(mismatches)} naming mismatches:{Colors.NC}\n")
  120. for i, mismatch in enumerate(mismatches, 1):
  121. print(f"{Colors.BOLD}Mismatch #{i}:{Colors.NC}")
  122. print(
  123. f" Backend uses: {Colors.YELLOW}\"{mismatch['backend_name']}\"{Colors.NC}"
  124. )
  125. print(
  126. f" Shader has: {Colors.GREEN}\"{mismatch['shader_name']}\"{Colors.NC}"
  127. )
  128. print(f" Affected shaders: {', '.join(mismatch['shaders'])}")
  129. print(f" Locations in backend.cpp:")
  130. for line_num, line in mismatch["locations"][:3]:
  131. print(f" Line {line_num}: {line}")
  132. if len(mismatch["locations"]) > 3:
  133. print(f" ... and {len(mismatch['locations']) - 3} more")
  134. print()
  135. print(f"{Colors.BOLD}=== Summary ==={Colors.NC}")
  136. print(f" {Colors.RED}Errors: {len(mismatches)}{Colors.NC}")
  137. print()
  138. print(
  139. f"{Colors.YELLOW}These mismatches will cause uniforms to not be found at runtime,"
  140. )
  141. print(f"resulting in rendering errors.{Colors.NC}")
  142. print()
  143. print(
  144. f"To fix: Update backend.cpp to use the exact uniform names from the shaders."
  145. )
  146. return 1 if mismatches else 0
  147. if __name__ == "__main__":
  148. sys.exit(main())