256 lines
8.2 KiB
Python
256 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
RenderDoc Capture Analysis Script
|
|
Uses RenderDoc's Python API to extract comprehensive information from .rdc capture files.
|
|
|
|
Run with: python analyze_capture.py <capture.rdc> [output.json]
|
|
|
|
Requires:
|
|
- Python 3.6
|
|
- renderdoc.pyd (compiled for Python 3.6)
|
|
- renderdoc.dll and dependencies in the same directory or PATH
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import os
|
|
|
|
RENDERDOC_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, RENDERDOC_DIR)
|
|
os.environ["PATH"] = RENDERDOC_DIR + os.pathsep + os.environ.get("PATH", "")
|
|
|
|
import renderdoc as rd
|
|
|
|
|
|
def safe_str(s):
|
|
if s is None:
|
|
return ""
|
|
try:
|
|
return str(s)
|
|
except:
|
|
return repr(s)
|
|
|
|
|
|
def extract_resource_info(resource):
|
|
return {
|
|
"resource_id": str(resource.resourceId),
|
|
"name": safe_str(resource.name),
|
|
"type": str(resource.type) if hasattr(resource, "type") else "Unknown",
|
|
"format": str(resource.format) if hasattr(resource, "format") else "Unknown",
|
|
"dimension": str(resource.dimension)
|
|
if hasattr(resource, "dimension")
|
|
else "Unknown",
|
|
"width": resource.width if hasattr(resource, "width") else 0,
|
|
"height": resource.height if hasattr(resource, "height") else 0,
|
|
"depth": resource.depth if hasattr(resource, "depth") else 0,
|
|
}
|
|
|
|
|
|
def extract_action_info(action, structured_file, include_children=True):
|
|
action_info = {
|
|
"event_id": action.eventId,
|
|
"name": safe_str(
|
|
action.GetName(structured_file)
|
|
if hasattr(action, "GetName")
|
|
else action.customName
|
|
),
|
|
"custom_name": safe_str(action.customName),
|
|
"flags": str(action.flags) if hasattr(action, "flags") else "NoFlags",
|
|
}
|
|
|
|
if hasattr(action, "numIndices") and action.numIndices > 0:
|
|
action_info["num_indices"] = action.numIndices
|
|
if hasattr(action, "numInstances") and action.numInstances > 0:
|
|
action_info["num_instances"] = action.numInstances
|
|
if hasattr(action, "vertexOffset"):
|
|
action_info["vertex_offset"] = action.vertexOffset
|
|
if hasattr(action, "indexOffset"):
|
|
action_info["index_offset"] = action.indexOffset
|
|
if hasattr(action, "baseVertex"):
|
|
action_info["base_vertex"] = action.baseVertex
|
|
|
|
if include_children and hasattr(action, "children") and action.children:
|
|
action_info["children"] = [
|
|
extract_action_info(child, structured_file, True)
|
|
for child in action.children
|
|
]
|
|
|
|
return action_info
|
|
|
|
|
|
def extract_shader_variables(controller, state):
|
|
shader_vars = []
|
|
|
|
shader_stages = [
|
|
(rd.ShaderStage.Vertex, "VS"),
|
|
(rd.ShaderStage.Pixel, "PS"),
|
|
(rd.ShaderStage.Compute, "CS"),
|
|
(rd.ShaderStage.Geometry, "GS"),
|
|
(rd.ShaderStage.Hull, "HS"),
|
|
(rd.ShaderStage.Domain, "DS"),
|
|
]
|
|
|
|
for stage, stage_name in shader_stages:
|
|
try:
|
|
shader = state.GetShader(stage)
|
|
if shader and hasattr(shader, "reflection") and shader.reflection:
|
|
cbuffers = shader.reflection.GetCBuffers()
|
|
for cbuf in cbuffers:
|
|
try:
|
|
variables = controller.GetCBufferVariableContents(
|
|
state, shader, cbuf.resourceId
|
|
)
|
|
if variables:
|
|
for var in variables:
|
|
var_info = {
|
|
"stage": stage_name,
|
|
"cbuffer": safe_str(cbuf.name),
|
|
"name": safe_str(var.name)
|
|
if hasattr(var, "name")
|
|
else "",
|
|
"type": str(var.type)
|
|
if hasattr(var, "type")
|
|
else "Unknown",
|
|
}
|
|
if hasattr(var, "value"):
|
|
var_info["value"] = str(var.value)[:500]
|
|
shader_vars.append(var_info)
|
|
except Exception as e:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
return shader_vars
|
|
|
|
|
|
def analyze_capture(filename):
|
|
if not os.path.exists(filename):
|
|
return {"error": f"File not found: {filename}"}
|
|
|
|
cap = rd.OpenCaptureFile()
|
|
result = cap.OpenFile(filename, "", None)
|
|
|
|
if result != rd.ResultCode.Succeeded:
|
|
return {"error": f"Couldn't open file: {result}"}
|
|
|
|
if not cap.LocalReplaySupport():
|
|
return {"error": "Capture cannot be replayed locally"}
|
|
|
|
result, controller = cap.OpenCapture(rd.ReplayOptions(), None)
|
|
|
|
if result != rd.ResultCode.Succeeded:
|
|
return {"error": f"Couldn't init replay: {result}"}
|
|
|
|
try:
|
|
structured_file = controller.GetStructuredFile()
|
|
|
|
capture_info = {
|
|
"filename": os.path.basename(filename),
|
|
"full_path": os.path.abspath(filename),
|
|
"file_size": os.path.getsize(filename),
|
|
"renderdoc_version": rd.GetVersionString(),
|
|
}
|
|
|
|
frame_info = {}
|
|
try:
|
|
frame_desc = controller.GetFrameInfo()
|
|
if frame_desc:
|
|
frame_info = {
|
|
"frame_number": frame_desc.frameNumber
|
|
if hasattr(frame_desc, "frameNumber")
|
|
else 0,
|
|
}
|
|
except:
|
|
pass
|
|
|
|
root_actions = controller.GetRootActions()
|
|
actions = []
|
|
draw_call_count = 0
|
|
|
|
def process_actions(action_list):
|
|
nonlocal draw_call_count
|
|
for action in action_list:
|
|
action_info = extract_action_info(action, structured_file, False)
|
|
if hasattr(action, "flags") and action.flags & rd.ActionFlags.Drawcall:
|
|
draw_call_count += 1
|
|
actions.append(action_info)
|
|
|
|
if root_actions:
|
|
process_actions(root_actions)
|
|
|
|
frame_info["draw_call_count"] = draw_call_count
|
|
|
|
resources = []
|
|
try:
|
|
resource_list = controller.GetResourceList()
|
|
if resource_list:
|
|
resources = [extract_resource_info(res) for res in resource_list]
|
|
except:
|
|
pass
|
|
|
|
state = controller.GetPipelineState()
|
|
shader_variables = extract_shader_variables(controller, state)
|
|
|
|
debug_messages = []
|
|
try:
|
|
messages = controller.GetDebugMessages()
|
|
if messages:
|
|
for msg in messages:
|
|
debug_messages.append(
|
|
{
|
|
"event_id": msg.eventId if hasattr(msg, "eventId") else 0,
|
|
"severity": str(msg.severity)
|
|
if hasattr(msg, "severity")
|
|
else "Unknown",
|
|
"category": str(msg.category)
|
|
if hasattr(msg, "category")
|
|
else "Unknown",
|
|
"message": safe_str(msg.description)
|
|
if hasattr(msg, "description")
|
|
else "",
|
|
}
|
|
)
|
|
except:
|
|
pass
|
|
|
|
result_data = {
|
|
"capture_info": capture_info,
|
|
"frame_info": frame_info,
|
|
"actions": actions,
|
|
"resources": resources,
|
|
"shader_variables": shader_variables,
|
|
"debug_messages": debug_messages,
|
|
}
|
|
|
|
finally:
|
|
controller.Shutdown()
|
|
cap.Shutdown()
|
|
rd.ShutdownReplay()
|
|
|
|
return result_data
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python analyze_capture.py <capture.rdc> [output.json]")
|
|
sys.exit(1)
|
|
|
|
filename = sys.argv[1]
|
|
output_path = sys.argv[2] if len(sys.argv) > 2 else None
|
|
|
|
print(f"Analyzing: {filename}", file=sys.stderr)
|
|
|
|
result = analyze_capture(filename)
|
|
|
|
if output_path:
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
json.dump(result, f, indent=2, ensure_ascii=False)
|
|
print(f"Saved to: {output_path}", file=sys.stderr)
|
|
else:
|
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|