275 lines
9.8 KiB
Python
275 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
着色器编译模块
|
||
|
||
负责调用glslc编译器将GLSL源代码编译为SPIR-V二进制。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional
|
||
|
||
from .constants import GLSLC_STD, GLSLC_TARGET_ENV
|
||
from .spirv_parser import extract_spirv_reflection
|
||
from .types import CompilationResult, ShaderMetadata, ToolError
|
||
|
||
LOG = logging.getLogger("glsl2spirv")
|
||
|
||
|
||
# ============ 着色器注释解析 ============
|
||
|
||
def parse_shader_annotations(source_code: str) -> Dict[str, Optional[int]]:
|
||
"""解析着色器源码中的特殊注释
|
||
|
||
支持的注释:
|
||
- @instance_buffer: 标记实例缓冲 SSBO
|
||
|
||
解析规则:
|
||
1. 查找 `// @instance_buffer` 或 `/* @instance_buffer */` 注释
|
||
2. 解析紧随其后的 layout 声明中的 binding 编号
|
||
|
||
Args:
|
||
source_code: 着色器源代码
|
||
|
||
Returns:
|
||
包含注释信息的字典:
|
||
- instance_buffer_binding: 标记为 @instance_buffer 的 binding 编号(或 None)
|
||
|
||
Example:
|
||
源码中的标记:
|
||
```glsl
|
||
// @instance_buffer
|
||
layout(std430, set = 0, binding = 0) readonly buffer InstanceBuffer {
|
||
InstanceData instances[];
|
||
};
|
||
```
|
||
返回: {'instance_buffer_binding': 0}
|
||
"""
|
||
annotations = {
|
||
'instance_buffer_binding': None,
|
||
}
|
||
|
||
lines = source_code.split('\n')
|
||
|
||
# 用于匹配 @instance_buffer 注释的正则
|
||
instance_buffer_comment_re = re.compile(
|
||
r'//\s*@instance_buffer|/\*\s*@instance_buffer\s*\*/'
|
||
)
|
||
|
||
# 用于从 layout 声明提取 binding 的正则
|
||
# 匹配: layout(..., binding = N, ...) 或 layout(binding=N)
|
||
binding_re = re.compile(r'binding\s*=\s*(\d+)')
|
||
|
||
for i, line in enumerate(lines):
|
||
if instance_buffer_comment_re.search(line):
|
||
# 找到 @instance_buffer 注释,查找后续的 layout 声明
|
||
# 可能在同一行(如果注释在行尾)或下一行
|
||
|
||
# 首先检查同一行(注释可能在 layout 之前)
|
||
match = binding_re.search(line)
|
||
if match:
|
||
annotations['instance_buffer_binding'] = int(match.group(1))
|
||
LOG.debug(f"Found @instance_buffer at line {i+1}, binding = {match.group(1)}")
|
||
continue
|
||
|
||
# 检查接下来的几行(跳过空行和注释)
|
||
for j in range(i + 1, min(i + 5, len(lines))):
|
||
next_line = lines[j].strip()
|
||
|
||
# 跳过空行和纯注释行
|
||
if not next_line or next_line.startswith('//'):
|
||
continue
|
||
|
||
# 查找 layout 声明中的 binding
|
||
match = binding_re.search(next_line)
|
||
if match:
|
||
annotations['instance_buffer_binding'] = int(match.group(1))
|
||
LOG.debug(f"Found @instance_buffer at line {i+1}, binding = {match.group(1)} (from line {j+1})")
|
||
break
|
||
|
||
# 如果找到了非空非注释行但没有 binding,停止搜索
|
||
if 'layout' not in next_line.lower():
|
||
break
|
||
|
||
# 只处理第一个 @instance_buffer 标记
|
||
if annotations['instance_buffer_binding'] is not None:
|
||
break
|
||
|
||
return annotations
|
||
|
||
|
||
class ShaderCompiler:
|
||
"""GLSL到SPIR-V编译器"""
|
||
|
||
def __init__(self, verbose: bool = False, dry_run: bool = False, debug_spirv: bool = False):
|
||
self.verbose = verbose
|
||
self.dry_run = dry_run
|
||
self.debug_spirv = debug_spirv
|
||
self.glslc_path = self._find_glslc()
|
||
|
||
def _find_glslc(self) -> str:
|
||
"""查找glslc编译器"""
|
||
glslc = shutil.which("glslc")
|
||
if glslc:
|
||
return glslc
|
||
|
||
glslc = shutil.which("glslc.exe")
|
||
if glslc:
|
||
return glslc
|
||
|
||
raise ToolError("Unable to locate glslc compiler. Please install Vulkan SDK.")
|
||
|
||
def _build_glslc_command(
|
||
self,
|
||
shader_path: Path,
|
||
output_path: Path,
|
||
metadata: ShaderMetadata,
|
||
shader_type: str,
|
||
debug_mode: bool = False,
|
||
) -> List[str]:
|
||
"""构建glslc命令行
|
||
|
||
Args:
|
||
shader_path: 着色器源文件路径
|
||
output_path: 输出SPIR-V文件路径
|
||
metadata: 着色器元数据
|
||
shader_type: 着色器类型(vert, frag, comp等)
|
||
debug_mode: 是否启用debug模式(用于提取完整反射数据)
|
||
"""
|
||
stage_map = {
|
||
'vert': 'vertex',
|
||
'frag': 'fragment',
|
||
'comp': 'compute',
|
||
'geom': 'geometry',
|
||
'tesc': 'tesscontrol',
|
||
'tese': 'tesseval',
|
||
}
|
||
|
||
cmd = [self.glslc_path]
|
||
|
||
if shader_type in stage_map:
|
||
cmd.append(f"-fshader-stage={stage_map[shader_type]}")
|
||
|
||
cmd.extend([
|
||
str(shader_path),
|
||
"-o", str(output_path),
|
||
"--target-env=" + GLSLC_TARGET_ENV,
|
||
"-std=" + GLSLC_STD,
|
||
])
|
||
|
||
# debug模式:启用调试信息和禁用优化以保留完整的反射数据
|
||
if debug_mode:
|
||
cmd.extend(["-g", "-O0"])
|
||
|
||
for inc_path in metadata.include_paths:
|
||
cmd.append(f"-I{inc_path}")
|
||
|
||
for macro_name, macro_value in metadata.defines.items():
|
||
if macro_value:
|
||
cmd.append(f"-D{macro_name}={macro_value}")
|
||
else:
|
||
cmd.append(f"-D{macro_name}")
|
||
|
||
return cmd
|
||
|
||
def _run_glslc(self, cmd: List[str], shader_path: Path, shader_type: str) -> None:
|
||
"""执行glslc命令"""
|
||
try:
|
||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||
if result.returncode != 0:
|
||
raise ToolError(
|
||
f"glslc compilation failed for {shader_path.name} ({shader_type}):\n{result.stderr}"
|
||
)
|
||
except FileNotFoundError:
|
||
raise ToolError(f"glslc executable not found at {self.glslc_path}")
|
||
|
||
def compile_shader(
|
||
self,
|
||
shader_path: Path,
|
||
metadata: ShaderMetadata,
|
||
shader_type: str,
|
||
) -> CompilationResult:
|
||
"""编译单个着色器并提取反射信息
|
||
|
||
编译流程:
|
||
1. 读取着色器源代码并解析特殊注释(如 @instance_buffer)
|
||
2. 用debug模式编译(-g -O0)以获取完整的反射数据
|
||
3. 如果启用了debug_spirv,则最终SPIR-V使用debug版本
|
||
否则用正常模式编译生成最终的优化SPIR-V
|
||
|
||
这样可以确保绑定代码生成使用完整的变量名信息,
|
||
同时在非debug构建时最终的着色器仍然是优化过的版本。
|
||
"""
|
||
# 读取着色器源代码并解析注释
|
||
source_code = shader_path.read_text(encoding='utf-8')
|
||
annotations = parse_shader_annotations(source_code)
|
||
|
||
if annotations.get('instance_buffer_binding') is not None:
|
||
LOG.info(f"Detected @instance_buffer annotation in {shader_path.name}, binding = {annotations['instance_buffer_binding']}")
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
debug_output_path = Path(tmpdir) / f"{shader_path.stem}_debug.spv"
|
||
release_output_path = Path(tmpdir) / f"{shader_path.stem}.spv"
|
||
|
||
if not self.dry_run:
|
||
# 步骤1:用debug模式编译以提取完整反射数据
|
||
debug_cmd = self._build_glslc_command(
|
||
shader_path, debug_output_path, metadata, shader_type, debug_mode=True
|
||
)
|
||
|
||
if self.verbose:
|
||
LOG.debug("Running glslc (debug): %s", " ".join(debug_cmd))
|
||
|
||
self._run_glslc(debug_cmd, shader_path, shader_type)
|
||
|
||
debug_spirv_data = debug_output_path.read_bytes()
|
||
|
||
# 从debug版本的SPIR-V提取完整反射信息,并传入注释信息
|
||
reflection_result = extract_spirv_reflection(
|
||
debug_spirv_data,
|
||
shader_type,
|
||
annotations=annotations,
|
||
source_file=shader_path
|
||
)
|
||
entry_point = reflection_result['entry_point']
|
||
bindings = reflection_result['bindings']
|
||
reflection = reflection_result.get('reflection')
|
||
|
||
# 步骤2:根据debug_spirv标志决定最终SPIR-V版本
|
||
if self.debug_spirv:
|
||
# 使用debug版本作为最终输出
|
||
spirv_data = debug_spirv_data
|
||
if self.verbose:
|
||
LOG.debug("Using debug SPIR-V as final output for %s", shader_path.name)
|
||
else:
|
||
# 用正常模式编译生成最终的优化SPIR-V
|
||
release_cmd = self._build_glslc_command(
|
||
shader_path, release_output_path, metadata, shader_type, debug_mode=False
|
||
)
|
||
|
||
if self.verbose:
|
||
LOG.debug("Running glslc (release): %s", " ".join(release_cmd))
|
||
|
||
self._run_glslc(release_cmd, shader_path, shader_type)
|
||
|
||
spirv_data = release_output_path.read_bytes()
|
||
else:
|
||
spirv_data = b""
|
||
entry_point = "main"
|
||
bindings = []
|
||
reflection = None
|
||
|
||
return CompilationResult(
|
||
source_file=shader_path,
|
||
shader_type=shader_type,
|
||
spirv_data=spirv_data,
|
||
entry_point=entry_point,
|
||
bindings=bindings,
|
||
reflection=reflection,
|
||
) |