Files
mirage/tools/compiler.py
2025-12-25 13:20:16 +08:00

275 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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,
)