Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ RenderDocMCP/
├── mcp_server/ # MCPサーバー
│ ├── server.py # FastMCPエントリーポイント
│ ├── config.py # 設定
│ ├── shader_compiler.py # モバイルGPUオフラインコンパイラ連携
│ └── bridge/
│ └── client.py # ファイルベースIPCクライアント
Expand All @@ -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 # 拡張機能インストール
```
Expand All @@ -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 フィルタリングオプション

Expand Down
74 changes: 71 additions & 3 deletions mcp_server/config.py
Original file line number Diff line number Diff line change
@@ -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()
130 changes: 130 additions & 0 deletions mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
Loading