diff --git a/CLAUDE.md b/CLAUDE.md index 27493f9..98f4416 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,7 @@ RenderDocMCP/ ├── mcp_server/ # MCPサーバー │ ├── server.py # FastMCPエントリーポイント │ ├── config.py # 設定 +│ ├── shader_compiler.py # モバイルGPUオフラインコンパイラ連携 │ └── bridge/ │ └── client.py # ファイルベースIPCクライアント │ @@ -33,6 +34,13 @@ RenderDocMCP/ │ ├── request_handler.py # リクエスト処理 │ └── renderdoc_facade.py # RenderDoc APIラッパー │ +├── mobile_offline_compilers/ # バンドル済みオフラインコンパイラ +│ ├── malioc.exe # Mali Offline Compiler +│ ├── aoc.exe # Adreno Offline Compiler +│ └── graphics/ # Mali GPU コアライブラリ +│ +├── renderdoc_mcp_config.json # コンパイラパス設定 +│ └── scripts/ └── install_extension.py # 拡張機能インストール ``` @@ -56,6 +64,29 @@ RenderDocMCP/ | `get_texture_info` | テクスチャメタデータ | | `get_texture_data` | テクスチャピクセルデータ取得(mip/slice/3Dスライス対応) | | `get_pipeline_state` | パイプライン状態全体 | +| `analyze_shader_performance` | モバイルGPUオフラインコンパイラでシェーダー性能分析(Mali/Adreno) | + +### analyze_shader_performance(シェーダー性能分析) + +モバイルGPUオフラインコンパイラ(malioc / aoc)を使い、シェーダーの性能指標を取得する。 +GLSL ソースは SPIR-V の `debugInfo.files`(`ShaderReflection.debugInfo.files[0].contents`)から自動抽出される。 + +```python +analyze_shader_performance( + event_id=979, + stage="fragment", # "vertex", "fragment"("pixel"), "compute" + compiler="both", # "mali", "adreno", "both" + mali_core="Mali-G78", # 省略時は設定デフォルト + adreno_arch="a650", # 省略時は設定デフォルト +) +# Mali 結果: サイクル数(arithmetic/load_store/texture/varying)、 +# ワーク/ユニフォームレジスタ数、スレッド占有率、FP16使用率、スタックスピル +# Adreno 結果: 命令数(ALU 32/16bit, texture, memory, flow control)、 +# レジスタ使用量(full/half precision)、ファイバー占有率 +``` + +**設定ファイル**: `renderdoc_mcp_config.json` でコンパイラパスとデフォルトターゲットを指定可能。 +コンパイラバイナリ(`malioc.exe`, `aoc.exe`)は `mobile_offline_compilers/` にバンドル済み。 ### get_draw_calls フィルタリングオプション diff --git a/mcp_server/config.py b/mcp_server/config.py index 9b8f82b..07acf1e 100644 --- a/mcp_server/config.py +++ b/mcp_server/config.py @@ -1,14 +1,82 @@ """Configuration for RenderDoc MCP Server""" +import json import os +from pathlib import Path + + +# Default config file search paths +_CONFIG_SEARCH_PATHS = [ + Path.cwd() / "renderdoc_mcp_config.json", + Path.home() / ".renderdoc_mcp" / "config.json", +] + +# Package root (parent of mcp_server/) +_PKG_ROOT = Path(__file__).parent.parent + + +def _find_config_file() -> Path | None: + """Search for config file in standard locations.""" + # Environment variable override + env_path = os.environ.get("RENDERDOC_MCP_CONFIG") + if env_path: + p = Path(env_path) + if p.is_file(): + return p + + for path in _CONFIG_SEARCH_PATHS: + if path.is_file(): + return path + + return None + + +def _load_json_config(path: Path) -> dict: + """Load JSON config file.""" + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} class Settings: - """Server settings""" + """Server settings with JSON config file and environment variable support.""" def __init__(self): - self.renderdoc_host = os.environ.get("RENDERDOC_MCP_HOST", "127.0.0.1") - self.renderdoc_port = int(os.environ.get("RENDERDOC_MCP_PORT", "19876")) + # Load JSON config if available + config_path = _find_config_file() + self._file_config = _load_json_config(config_path) if config_path else {} + self._config_path = config_path + + # --- RenderDoc Bridge --- + self.renderdoc_host = self._get("renderdoc_host", "RENDERDOC_MCP_HOST", "127.0.0.1") + self.renderdoc_port = int(self._get("renderdoc_port", "RENDERDOC_MCP_PORT", "19876")) + + # --- Compiler Paths --- + compiler_dir = _PKG_ROOT / "mobile_offline_compilers" + self.malioc_path = self._get( + "malioc_path", "MALIOC_PATH", + str(compiler_dir / "malioc.exe"), + ) + self.aoc_path = self._get( + "aoc_path", "AOC_PATH", + str(compiler_dir / "aoc.exe"), + ) + + # --- Default GPU Targets --- + self.mali_default_core = self._get("mali_default_core", "MALI_DEFAULT_CORE", "Mali-G78") + self.adreno_default_arch = self._get("adreno_default_arch", "ADRENO_DEFAULT_ARCH", "a650") + + def _get(self, key: str, env_key: str, default: str) -> str: + """Get a setting value. Priority: env var > JSON config > default.""" + env_val = os.environ.get(env_key) + if env_val is not None: + return env_val + file_val = self._file_config.get(key) + if file_val is not None: + return str(file_val) + return default settings = Settings() diff --git a/mcp_server/server.py b/mcp_server/server.py index 92508ef..2b3a78e 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -9,6 +9,7 @@ from .bridge.client import RenderDocBridge, RenderDocBridgeError from .config import settings +from .shader_compiler import ShaderPerformanceAnalyzer, ShaderCompilerError # Initialize FastMCP server mcp = FastMCP( @@ -18,6 +19,22 @@ # RenderDoc bridge client bridge = RenderDocBridge(host=settings.renderdoc_host, port=settings.renderdoc_port) +# Shader performance analyzer (lazy init to avoid startup errors if compilers missing) +_analyzer: ShaderPerformanceAnalyzer | None = None + + +def _get_analyzer() -> ShaderPerformanceAnalyzer: + """Get or create the shader performance analyzer.""" + global _analyzer + if _analyzer is None: + _analyzer = ShaderPerformanceAnalyzer( + malioc_path=settings.malioc_path, + aoc_path=settings.aoc_path, + mali_default_core=settings.mali_default_core, + adreno_default_arch=settings.adreno_default_arch, + ) + return _analyzer + @mcp.tool def get_capture_status() -> dict: @@ -311,6 +328,119 @@ def open_capture(capture_path: str) -> dict: return bridge.call("open_capture", {"capture_path": capture_path}) +@mcp.tool +def analyze_shader_performance( + event_id: int, + stage: Literal["vertex", "fragment", "pixel", "compute"], + compiler: Literal["mali", "adreno", "both"] = "both", + mali_core: str | None = None, + adreno_arch: str | None = None, +) -> dict: + """ + Analyze shader performance using mobile GPU offline compilers. + + Compiles the shader from the specified draw call through Mali Offline Compiler + (malioc) and/or Adreno Offline Compiler (aoc), returning detailed performance + metrics including instruction counts, cycle counts, register usage, and + thread/fiber occupancy. + + Only works with OpenGL ES (GLSL) captures. The shader source is extracted + from the loaded RenderDoc capture via disassembly or embedded debug info. + + Args: + event_id: The event ID of the draw call to analyze + stage: Shader stage - "vertex", "fragment" (or "pixel"), "compute" + compiler: Which compiler to use - "mali", "adreno", or "both" (default) + mali_core: Mali GPU core target (e.g. "Mali-G78", "Mali-G720"). + Uses config default if not specified. + adreno_arch: Adreno architecture target (e.g. "a650", "a740"). + Uses config default if not specified. + + Returns: + Performance analysis results from the requested compiler(s), including: + - Mali: cycle counts per pipeline (arithmetic, load/store, texture, varying), + work/uniform registers, thread occupancy, FP16 arithmetic %, stack spilling + - Adreno: instruction counts (ALU 32/16-bit, texture, memory, flow control), + register footprint (full/half precision), fiber occupancy + """ + # Normalize stage name: bridge expects "pixel" not "fragment" + bridge_stage = "pixel" if stage in ("fragment", "pixel") else stage + + source = None + resource_id = "" + entry_point = "" + selected_target = "" + available_targets = [] + + # Try to get shader source from capture, with fallback strategies + export_error = None + + # Strategy 1: Try export_shader_source (auto-selects best target, + # also checks reflection.debugInfo.files for embedded GLSL) + try: + shader_data = bridge.call("export_shader_source", {"event_id": event_id, "stage": bridge_stage}) + if shader_data and isinstance(shader_data, dict): + source = shader_data.get("source") + resource_id = shader_data.get("resource_id", "") + entry_point = shader_data.get("entry_point", "") + selected_target = shader_data.get("selected_target", "") + available_targets = shader_data.get("available_targets", []) + except Exception as e: + export_error = str(e) + + # Strategy 2: Fallback to get_shader_info disassembly + if not source: + try: + shader_info = bridge.call("get_shader_info", {"event_id": event_id, "stage": bridge_stage}) + if shader_info and isinstance(shader_info, dict): + source = shader_info.get("disassembly") + resource_id = shader_info.get("resource_id", "") + entry_point = shader_info.get("entry_point", "") + selected_target = "get_shader_info fallback" + except Exception as e: + raise ValueError( + f"Cannot get shader source for event {event_id} stage '{stage}': {e}" + ) + + # Validate it looks like GLSL (not SPIR-V disassembly) + if source and (source.strip().startswith("SPIR-V") or source.strip().startswith("; SPIR-V")): + raise ValueError( + f"The shader source is SPIR-V disassembly (target: '{selected_target}'), " + "which cannot be compiled by mobile offline compilers. " + f"Available targets: {available_targets}. " + "This tool requires compilable GLSL source code." + ) + + if not source: + diag = f"export_shader_source error: {export_error}" if export_error else "export_shader_source returned no source" + raise ValueError( + f"Cannot get shader source for event {event_id} stage '{stage}'. " + f"Diagnostics: {diag}. " + f"Available disassembly targets: {available_targets}. " + "This tool requires compilable GLSL source code " + "(typically available in OpenGL ES captures with debug info)." + ) + + # Run offline compiler analysis + analyzer = _get_analyzer() + result = analyzer.analyze( + shader_source=source, + stage=stage, + compiler=compiler, + mali_core=mali_core, + adreno_arch=adreno_arch, + ) + + # Add context info + result["event_id"] = event_id + result["shader_resource_id"] = resource_id + result["entry_point"] = entry_point + result["disassembly_target"] = selected_target + result["available_targets"] = available_targets + + return result + + def main(): """Run the MCP server""" mcp.run() diff --git a/mcp_server/shader_compiler.py b/mcp_server/shader_compiler.py new file mode 100644 index 0000000..b789adf --- /dev/null +++ b/mcp_server/shader_compiler.py @@ -0,0 +1,420 @@ +""" +Mobile GPU Offline Shader Compiler Integration. + +Wraps Mali Offline Compiler (malioc) and Adreno Offline Compiler (aoc) +to provide shader performance analysis for mobile GPUs. +""" + +import json +import os +import re +import subprocess +import tempfile +from pathlib import Path +from typing import Any + + +# Stage name to file extension mapping +_STAGE_EXT_MAP = { + "vertex": ".vert", + "fragment": ".frag", + "pixel": ".frag", + "compute": ".comp", + "geometry": ".geom", + "hull": ".tesc", + "domain": ".tese", +} + +# Stage name to malioc flag mapping +_STAGE_MALIOC_FLAG = { + "vertex": "--vertex", + "fragment": "--fragment", + "pixel": "--fragment", + "compute": "--compute", + "geometry": "--geometry", + "hull": "--tessellation_control", + "domain": "--tessellation_evaluation", +} + + +class ShaderCompilerError(Exception): + """Error during shader compilation/analysis.""" + pass + + +class MaliCompiler: + """Wrapper for Mali Offline Compiler (malioc).""" + + def __init__(self, malioc_path: str, default_core: str = "Mali-G78"): + self.path = malioc_path + self.default_core = default_core + self._validate() + + def _validate(self): + if not os.path.isfile(self.path): + raise ShaderCompilerError(f"malioc not found at: {self.path}") + + def analyze( + self, + shader_source: str, + stage: str, + core: str | None = None, + ) -> dict[str, Any]: + """ + Analyze a GLSL shader using malioc. + + Args: + shader_source: Complete GLSL source code + stage: Shader stage (vertex, fragment, pixel, compute, etc.) + core: Target Mali GPU core (e.g. "Mali-G78"). Uses default if None. + + Returns: + Structured performance analysis result. + """ + core = core or self.default_core + ext = _STAGE_EXT_MAP.get(stage, ".frag") + stage_flag = _STAGE_MALIOC_FLAG.get(stage, "--fragment") + + tmp_path = None + try: + # Write shader to temp file + tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="rdmcp_mali_") + with os.fdopen(tmp_fd, "w", encoding="utf-8") as f: + f.write(shader_source) + + # Run malioc + cmd = [ + self.path, + "--format", "json", + stage_flag, + "--opengles", + "-c", core, + tmp_path, + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() or result.stdout.strip() + raise ShaderCompilerError(f"malioc failed (exit {result.returncode}): {error_msg}") + + # Parse JSON output + raw = json.loads(result.stdout) + return self._parse_output(raw, core) + + except json.JSONDecodeError as e: + raise ShaderCompilerError(f"Failed to parse malioc JSON output: {e}") + except subprocess.TimeoutExpired: + raise ShaderCompilerError("malioc timed out after 30 seconds") + except ShaderCompilerError: + raise + except Exception as e: + raise ShaderCompilerError(f"malioc error: {e}") + finally: + if tmp_path and os.path.exists(tmp_path): + os.remove(tmp_path) + + def _parse_output(self, raw: dict, core: str) -> dict[str, Any]: + """Parse malioc JSON output into a clean structure.""" + result: dict[str, Any] = { + "compiler": "mali", + "compiler_version": ".".join(str(v) for v in raw.get("producer", {}).get("version", [])), + "core": core, + } + + shaders = raw.get("shaders", []) + if not shaders: + raise ShaderCompilerError("No shader analysis results from malioc") + + shader = shaders[0] + hw = shader.get("hardware", {}) + result["architecture"] = hw.get("architecture", "") + result["revision"] = hw.get("revision", "") + + shader_meta = shader.get("shader", {}) + result["api"] = shader_meta.get("api", "") + result["shader_type"] = shader_meta.get("type", "") + + # Parse variants + variants = [] + for variant in shader.get("variants", []): + v_result: dict[str, Any] = {"name": variant.get("name", "")} + + # Performance cycles + perf = variant.get("performance", {}) + total = perf.get("total_cycles", {}) + pipelines = perf.get("pipelines", []) + cycles = total.get("cycle_count", []) + bound = total.get("bound_pipelines", []) + + cycle_detail = {} + for i, pipe_name in enumerate(pipelines): + if i < len(cycles): + cycle_detail[pipe_name] = cycles[i] + v_result["cycles"] = cycle_detail + v_result["bound_pipelines"] = bound + + # Shortest / longest path + shortest = perf.get("shortest_path_cycles", {}) + longest = perf.get("longest_path_cycles", {}) + if shortest.get("cycle_count"): + s_cycles = {} + for i, pipe_name in enumerate(pipelines): + if i < len(shortest["cycle_count"]): + s_cycles[pipe_name] = shortest["cycle_count"][i] + v_result["shortest_path_cycles"] = s_cycles + if longest.get("cycle_count"): + l_cycles = {} + for i, pipe_name in enumerate(pipelines): + if i < len(longest["cycle_count"]): + l_cycles[pipe_name] = longest["cycle_count"][i] + v_result["longest_path_cycles"] = l_cycles + + # Properties + props = {} + for prop in variant.get("properties", []): + props[prop["name"]] = prop.get("value") + v_result["work_registers"] = props.get("work_registers_used") + v_result["uniform_registers"] = props.get("uniform_registers_used") + v_result["thread_occupancy"] = props.get("thread_occupancy") + v_result["has_stack_spilling"] = props.get("has_stack_spilling", False) + v_result["stack_spill_bytes"] = props.get("stack_spill_bytes", 0) + v_result["fp16_arithmetic_pct"] = props.get("fp16_arithmetic") + + variants.append(v_result) + + result["variants"] = variants + + # Shader-level properties + shader_props = {} + for prop in shader.get("properties", []): + shader_props[prop["name"]] = prop.get("value") + result["has_uniform_computation"] = shader_props.get("has_uniform_computation", False) + result["has_side_effects"] = shader_props.get("has_side_effects", False) + result["modifies_coverage"] = shader_props.get("modifies_coverage", False) + result["uses_late_zs_test"] = shader_props.get("uses_late_zs_test", False) + result["uses_late_zs_update"] = shader_props.get("uses_late_zs_update", False) + + # Warnings + warnings = shader.get("warnings", []) + if warnings: + result["warnings"] = warnings + + return result + + +class AdrenoCompiler: + """Wrapper for Adreno Offline Compiler (aoc).""" + + def __init__(self, aoc_path: str, default_arch: str = "a650"): + self.path = aoc_path + self.default_arch = default_arch + self._validate() + + def _validate(self): + if not os.path.isfile(self.path): + raise ShaderCompilerError(f"aoc not found at: {self.path}") + + def analyze( + self, + shader_source: str, + stage: str, + arch: str | None = None, + ) -> dict[str, Any]: + """ + Analyze a GLSL shader using aoc. + + Args: + shader_source: Complete GLSL source code + stage: Shader stage (vertex, fragment, pixel, compute, etc.) + arch: Target Adreno architecture (e.g. "a650"). Uses default if None. + + Returns: + Structured performance analysis result. + """ + arch = arch or self.default_arch + ext = _STAGE_EXT_MAP.get(stage, ".frag") + + tmp_path = None + try: + tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="rdmcp_aoc_") + with os.fdopen(tmp_fd, "w", encoding="utf-8") as f: + f.write(shader_source) + + cmd = [ + self.path, + f"-arch={arch}", + tmp_path, + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() or result.stdout.strip() + raise ShaderCompilerError(f"aoc failed (exit {result.returncode}): {error_msg}") + + # Check for compilation failure in stdout + if "Compilation failed" in result.stdout: + raise ShaderCompilerError(f"aoc compilation failed: {result.stdout.strip()}") + + return self._parse_output(result.stdout, arch, stage) + + except subprocess.TimeoutExpired: + raise ShaderCompilerError("aoc timed out after 30 seconds") + except ShaderCompilerError: + raise + except Exception as e: + raise ShaderCompilerError(f"aoc error: {e}") + finally: + if tmp_path and os.path.exists(tmp_path): + os.remove(tmp_path) + + def _parse_output(self, stdout: str, arch: str, stage: str) -> dict[str, Any]: + """Parse aoc text output into a structured result.""" + # Extract version info + version_match = re.search(r"AOC Version\s*:\s*([\d.]+)", stdout) + compiler_ver_match = re.search(r"Compiler Version\s*:\s*(\S+)", stdout) + + result: dict[str, Any] = { + "compiler": "adreno", + "compiler_version": version_match.group(1) if version_match else "", + "internal_compiler_version": compiler_ver_match.group(1) if compiler_ver_match else "", + "arch": arch, + } + + # Parse shader stats sections + # AOC outputs one or more sections like "============ Shader Stats FS ============" + section_pattern = r"={3,}\s*Shader Stats\s+(.+?)\s*={3,}(.*?)(?=={3,}\s*Shader Stats|Compilation succeeded|$)" + sections = re.findall(section_pattern, stdout, re.DOTALL) + + shader_sections = [] + for section_name, section_body in sections: + stats = self._parse_stats_section(section_body.strip()) + stats["section"] = section_name.strip() + shader_sections.append(stats) + + if shader_sections: + # Primary section is the first one (e.g. "FS", "VS") + result["stats"] = shader_sections[0] + if len(shader_sections) > 1: + result["additional_sections"] = shader_sections[1:] + + return result + + def _parse_stats_section(self, body: str) -> dict[str, Any]: + """Parse a single stats section from aoc output.""" + stats: dict[str, Any] = {} + # Each line: "Key description : value" + line_pattern = re.compile(r"^(.+?)\s*:\s*(\d+(?:\.\d+)?)\s*$", re.MULTILINE) + for match in line_pattern.finditer(body): + key = match.group(1).strip() + value_str = match.group(2) + # Convert to int or float + value: int | float = int(value_str) if "." not in value_str else float(value_str) + # Normalize key name + norm_key = self._normalize_key(key) + stats[norm_key] = value + + return stats + + @staticmethod + def _normalize_key(key: str) -> str: + """Normalize an aoc stat key to snake_case.""" + key = key.strip().lower() + key = re.sub(r"[^a-z0-9]+", "_", key) + key = key.strip("_") + return key + + +class ShaderPerformanceAnalyzer: + """ + High-level analyzer that orchestrates Mali and Adreno offline compilers. + """ + + def __init__( + self, + malioc_path: str | None = None, + aoc_path: str | None = None, + mali_default_core: str = "Mali-G78", + adreno_default_arch: str = "a650", + ): + self._mali: MaliCompiler | None = None + self._adreno: AdrenoCompiler | None = None + + if malioc_path and os.path.isfile(malioc_path): + self._mali = MaliCompiler(malioc_path, mali_default_core) + + if aoc_path and os.path.isfile(aoc_path): + self._adreno = AdrenoCompiler(aoc_path, adreno_default_arch) + + @property + def mali_available(self) -> bool: + return self._mali is not None + + @property + def adreno_available(self) -> bool: + return self._adreno is not None + + def analyze( + self, + shader_source: str, + stage: str, + compiler: str = "both", + mali_core: str | None = None, + adreno_arch: str | None = None, + ) -> dict[str, Any]: + """ + Analyze shader performance using mobile offline compilers. + + Args: + shader_source: Complete GLSL source code + stage: Shader stage name + compiler: "mali", "adreno", or "both" + mali_core: Override Mali GPU core target + adreno_arch: Override Adreno architecture target + + Returns: + Combined analysis results from requested compilers. + """ + result: dict[str, Any] = {"stage": stage} + errors: list[str] = [] + + run_mali = compiler in ("mali", "both") + run_adreno = compiler in ("adreno", "both") + + if run_mali: + if self._mali: + try: + result["mali"] = self._mali.analyze(shader_source, stage, mali_core) + except ShaderCompilerError as e: + errors.append(f"Mali: {e}") + result["mali"] = {"error": str(e)} + else: + errors.append("Mali offline compiler (malioc) not available") + result["mali"] = {"error": "malioc not found"} + + if run_adreno: + if self._adreno: + try: + result["adreno"] = self._adreno.analyze(shader_source, stage, adreno_arch) + except ShaderCompilerError as e: + errors.append(f"Adreno: {e}") + result["adreno"] = {"error": str(e)} + else: + errors.append("Adreno offline compiler (aoc) not available") + result["adreno"] = {"error": "aoc not found"} + + if errors: + result["errors"] = errors + + return result diff --git a/mobile_offline_compilers/__init__.py b/mobile_offline_compilers/__init__.py new file mode 100644 index 0000000..2991e67 --- /dev/null +++ b/mobile_offline_compilers/__init__.py @@ -0,0 +1 @@ +"""Mobile GPU Offline Shader Compilers""" diff --git a/mobile_offline_compilers/aoc.exe b/mobile_offline_compilers/aoc.exe new file mode 100644 index 0000000..ee3b708 Binary files /dev/null and b/mobile_offline_compilers/aoc.exe differ diff --git a/mobile_offline_compilers/graphics/Mali-Gxx_r44p0-00rel0.dll b/mobile_offline_compilers/graphics/Mali-Gxx_r44p0-00rel0.dll new file mode 100644 index 0000000..d2d7372 Binary files /dev/null and b/mobile_offline_compilers/graphics/Mali-Gxx_r44p0-00rel0.dll differ diff --git a/mobile_offline_compilers/graphics/Mali-T600_r23p0-00rel0.dll b/mobile_offline_compilers/graphics/Mali-T600_r23p0-00rel0.dll new file mode 100644 index 0000000..9dd6d29 Binary files /dev/null and b/mobile_offline_compilers/graphics/Mali-T600_r23p0-00rel0.dll differ diff --git a/mobile_offline_compilers/graphics/libMali-Gxx_r44p0-00rel0.dylib b/mobile_offline_compilers/graphics/libMali-Gxx_r44p0-00rel0.dylib new file mode 100644 index 0000000..b041ece Binary files /dev/null and b/mobile_offline_compilers/graphics/libMali-Gxx_r44p0-00rel0.dylib differ diff --git a/mobile_offline_compilers/graphics/libMali-T600_r23p0-00rel0.dylib b/mobile_offline_compilers/graphics/libMali-T600_r23p0-00rel0.dylib new file mode 100644 index 0000000..45fce86 Binary files /dev/null and b/mobile_offline_compilers/graphics/libMali-T600_r23p0-00rel0.dylib differ diff --git a/mobile_offline_compilers/malioc.exe b/mobile_offline_compilers/malioc.exe new file mode 100644 index 0000000..a054632 Binary files /dev/null and b/mobile_offline_compilers/malioc.exe differ diff --git a/pyproject.toml b/pyproject.toml index f72fa5f..c1f91ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["mcp_server"] +packages = ["mcp_server", "mobile_offline_compilers"] +artifacts = ["mobile_offline_compilers/**"] [tool.uv] package = true diff --git a/renderdoc_extension/renderdoc_facade.py b/renderdoc_extension/renderdoc_facade.py index a501b89..18e9b03 100644 --- a/renderdoc_extension/renderdoc_facade.py +++ b/renderdoc_extension/renderdoc_facade.py @@ -132,6 +132,10 @@ def get_shader_info(self, event_id, stage): """Get shader information for a specific stage""" return self._pipeline.get_shader_info(event_id, stage) + def export_shader_source(self, event_id, stage, target_name=None): + """Export shader source for offline compilation""" + return self._pipeline.export_shader_source(event_id, stage, target_name) + def get_pipeline_state(self, event_id): """Get full pipeline state at an event""" return self._pipeline.get_pipeline_state(event_id) diff --git a/renderdoc_extension/request_handler.py b/renderdoc_extension/request_handler.py index cd728aa..78a50fb 100644 --- a/renderdoc_extension/request_handler.py +++ b/renderdoc_extension/request_handler.py @@ -22,6 +22,7 @@ def __init__(self, facade): "get_draw_call_details": self._handle_get_draw_call_details, "get_action_timings": self._handle_get_action_timings, "get_shader_info": self._handle_get_shader_info, + "export_shader_source": self._handle_export_shader_source, "get_buffer_contents": self._handle_get_buffer_contents, "get_texture_info": self._handle_get_texture_info, "get_texture_data": self._handle_get_texture_data, @@ -136,6 +137,17 @@ def _handle_get_shader_info(self, params): raise ValueError("stage is required") return self.facade.get_shader_info(int(event_id), stage) + def _handle_export_shader_source(self, params): + """Handle export_shader_source request""" + event_id = params.get("event_id") + stage = params.get("stage") + if event_id is None: + raise ValueError("event_id is required") + if stage is None: + raise ValueError("stage is required") + target_name = params.get("target_name") + return self.facade.export_shader_source(int(event_id), stage, target_name) + def _handle_get_buffer_contents(self, params): """Handle get_buffer_contents request""" resource_id = params.get("resource_id") diff --git a/renderdoc_extension/services/pipeline_service.py b/renderdoc_extension/services/pipeline_service.py index 6dd6bd8..9365bbc 100644 --- a/renderdoc_extension/services/pipeline_service.py +++ b/renderdoc_extension/services/pipeline_service.py @@ -67,6 +67,125 @@ def callback(controller): raise ValueError(result["error"]) return result["shader"] + def export_shader_source(self, event_id, stage, target_name=None): + """Export shader source code for offline compilation. + + Attempts to get GLSL source code through multiple strategies: + 1. Check ShaderReflection.debugInfo.files for embedded source (e.g. from SPIR-V debug info) + 2. Look for a GLSL/source disassembly target + 3. Fall back to the default disassembly target + + Args: + event_id: The event ID to get shader source at + stage: Shader stage name + target_name: Optional specific disassembly target name to use. + If None, auto-selects the best target for compilation. + + Returns: + Dict with shader source, selected target, and all available targets. + """ + if not self.ctx.IsCaptureLoaded(): + raise ValueError("No capture loaded") + + result = {"data": None, "error": None} + + def callback(controller): + controller.SetFrameEvent(event_id, True) + + pipe = controller.GetPipelineState() + stage_enum = Parsers.parse_stage(stage) + + shader = pipe.GetShader(stage_enum) + if shader == rd.ResourceId.Null(): + result["error"] = "No %s shader bound" % stage + return + + reflection = pipe.GetShaderReflection(stage_enum) + entry = pipe.GetShaderEntryPoint(stage_enum) + pipeline_obj = pipe.GetGraphicsPipelineObject() + + targets = controller.GetDisassemblyTargets(True) + target_list = [str(t) for t in targets] if targets else [] + + # Strategy 1: Try to get embedded GLSL source from debugInfo.files + # This works for SPIR-V shaders with embedded GLSL debug info + # (e.g. DebugCompilationUnit / OpSource directives) + embedded_source = None + embedded_filename = None + try: + if reflection and reflection.debugInfo: + debug_files = reflection.debugInfo.files + if debug_files and len(debug_files) > 0: + # The first file is typically the entry point source + main_file = debug_files[0] + if main_file.contents and main_file.contents.strip(): + embedded_source = main_file.contents + embedded_filename = main_file.filename + except Exception: + pass # debugInfo may not be available for all shader types + + if embedded_source: + result["data"] = { + "resource_id": str(shader), + "entry_point": entry, + "stage": stage, + "selected_target": "debugInfo: %s" % (embedded_filename or "embedded"), + "available_targets": target_list, + "source": embedded_source, + } + return + + # Strategy 2: Try disassembly targets (prefer GLSL over SPIR-V) + if not targets: + result["error"] = "No disassembly targets available and no embedded source found" + return + + selected = None + if target_name: + # User specified a target + for t in targets: + if str(t) == target_name: + selected = t + break + if selected is None: + result["error"] = ( + "Target '%s' not found. Available: %s" + % (target_name, ", ".join(target_list)) + ) + return + else: + # Auto-select: prefer GLSL source over SPIR-V disassembly + selected = targets[0] # fallback + for t in targets: + t_str = str(t).lower() + if "glsl" in t_str or "source" in t_str: + selected = t + break + + # Get the disassembly for the selected target + try: + source = controller.DisassembleShader( + pipeline_obj, reflection, selected + ) + except Exception as e: + result["error"] = "DisassembleShader failed for '%s': %s" % (selected, e) + return + + result["data"] = { + "resource_id": str(shader), + "entry_point": entry, + "stage": stage, + "selected_target": str(selected), + "available_targets": target_list, + "source": source, + } + + self._invoke(callback) + + if result["error"]: + raise ValueError(result["error"]) + return result["data"] + def get_pipeline_state(self, event_id): """Get full pipeline state at an event""" if not self.ctx.IsCaptureLoaded(): diff --git a/renderdoc_mcp_config.json b/renderdoc_mcp_config.json new file mode 100644 index 0000000..832228f --- /dev/null +++ b/renderdoc_mcp_config.json @@ -0,0 +1,4 @@ +{ + "mali_default_core": "Mali-G78", + "adreno_default_arch": "a650" +}