import os import shutil import struct import subprocess # Mixed into DevCommand (bin/dev), which provides _project_root. class ShadersCommand: SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese") def shaders(self, *args, **kwargs): shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders") if not os.path.isdir(shaders_dir): raise ValueError(f"Shaders directory not found: {shaders_dir}") glslc = self._find_glslc() shader_paths = sorted( os.path.join(root, filename) for root, _dirs, files in os.walk(shaders_dir) for filename in files if filename.endswith(self.SHADER_EXTENSIONS) ) if not shader_paths: print(f"No shaders found in {shaders_dir}") return for shader_path in shader_paths: self._generate_shader_header(glslc, shader_path) def _find_glslc(self): glslc = shutil.which("glslc") if glslc is None: raise RuntimeError( "glslc not found on PATH (install the Vulkan SDK)" ) return glslc def _generate_shader_header(self, glslc, shader_path): relative_path = os.path.relpath(shader_path, self._project_root()) print(f"Compiling shader: {relative_path}") result = subprocess.run( [glslc, shader_path, "-o", "-"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if result.returncode != 0: raise RuntimeError( f"failed to compile {shader_path}: {result.stderr.decode()}" ) spirv = result.stdout if len(spirv) % 4 != 0: raise RuntimeError( f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)" ) word_count = len(spirv) // 4 words = struct.unpack(f"<{word_count}I", spirv) filename = os.path.basename(shader_path) variable_name = f"{filename.replace('.', '_')}_spv" header_path = shader_path + ".spv.h" body_lines = [ " " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + "," for i in range(0, len(words), 4) ] header = "\n".join([ "#pragma once", "", "#include ", "#include ", "", "namespace sophia::shaders {", "", f"inline constexpr std::array {variable_name} = {{", *body_lines, "};", "", "} // namespace sophia::shaders", "", ]) with open(header_path, "w") as f: f.write(header) print(f"Wrote {os.path.relpath(header_path, self._project_root())} " f"({word_count * 4} bytes)")