"""Utility functions: renderdoc module loading, serialization helpers, enum mappings.""" import json import os import sys from typing import Any def load_renderdoc(): """Load the renderdoc Python module from third_party directory.""" if "renderdoc" in sys.modules: return sys.modules["renderdoc"] util_dir = os.path.dirname(os.path.abspath(__file__)) third_party_dir = os.path.normpath( os.path.join(util_dir, "..", "..", "third_party", "renderdoc") ) pyd_path = os.path.join(third_party_dir, "renderdoc.pyd") if sys.platform == "win32" and hasattr(os, "add_dll_directory"): os.add_dll_directory(third_party_dir) sys.path.insert(0, third_party_dir) import renderdoc # noqa: E402 return renderdoc rd = load_renderdoc() # ── Shader stage mapping ── SHADER_STAGE_MAP = { "vertex": rd.ShaderStage.Vertex, "hull": rd.ShaderStage.Hull, "domain": rd.ShaderStage.Domain, "geometry": rd.ShaderStage.Geometry, "pixel": rd.ShaderStage.Pixel, "compute": rd.ShaderStage.Compute, } # ── FileType mapping ── FILE_TYPE_MAP = { "png": rd.FileType.PNG, "jpg": rd.FileType.JPG, "bmp": rd.FileType.BMP, "tga": rd.FileType.TGA, "hdr": rd.FileType.HDR, "exr": rd.FileType.EXR, "dds": rd.FileType.DDS, } # ── MeshDataStage mapping ── MESH_DATA_STAGE_MAP = { "vsin": rd.MeshDataStage.VSIn, "vsout": rd.MeshDataStage.VSOut, "gsout": rd.MeshDataStage.GSOut, } # ── Enum readable-name mappings ── BLEND_FACTOR_MAP = { 0: "Zero", 1: "One", 2: "SrcColor", 3: "InvSrcColor", 4: "DstColor", 5: "InvDstColor", 6: "SrcAlpha", 7: "InvSrcAlpha", 8: "DstAlpha", 9: "InvDstAlpha", 10: "SrcAlphaSat", 11: "BlendFactor", 12: "InvBlendFactor", 13: "Src1Color", 14: "InvSrc1Color", 15: "Src1Alpha", 16: "InvSrc1Alpha", } BLEND_OP_MAP = { 0: "Add", 1: "Subtract", 2: "RevSubtract", 3: "Min", 4: "Max", } COMPARE_FUNC_MAP = { 0: "AlwaysFalse", 1: "Never", 2: "Less", 3: "LessEqual", 4: "Greater", 5: "GreaterEqual", 6: "Equal", 7: "NotEqual", 8: "Always", } STENCIL_OP_MAP = { 0: "Keep", 1: "Zero", 2: "Replace", 3: "IncrSat", 4: "DecrSat", 5: "Invert", 6: "IncrWrap", 7: "DecrWrap", } CULL_MODE_MAP = {0: "None", 1: "Front", 2: "Back", 3: "FrontAndBack"} FILL_MODE_MAP = {0: "Solid", 1: "Wireframe", 2: "Point"} TOPOLOGY_MAP = { 0: "Unknown", 1: "PointList", 2: "LineList", 3: "LineStrip", 4: "TriangleList", 5: "TriangleStrip", 6: "TriangleFan", 7: "LineList_Adj", 8: "LineStrip_Adj", 9: "TriangleList_Adj", 10: "TriangleStrip_Adj", 11: "PatchList", } VAR_TYPE_MAP = { 0: "Float", 1: "Double", 2: "Half", 3: "SInt", 4: "UInt", 5: "SShort", 6: "UShort", 7: "SLong", 8: "ULong", 9: "SByte", 10: "UByte", 11: "Bool", 12: "Enum", 13: "GPUPointer", 14: "ConstantBlock", 15: "Struct", 16: "Unknown", } SYSTEM_VALUE_MAP = { 0: "None", 1: "Position", 2: "ClipDistance", 3: "CullDistance", 4: "RTIndex", 5: "ViewportIndex", 6: "VertexIndex", 7: "PrimitiveIndex", 8: "InstanceIndex", 9: "DispatchThreadIndex", 10: "GroupIndex", 11: "GroupFlatIndex", 12: "GroupThreadIndex", 13: "GSInstanceIndex", 14: "OutputControlPointIndex", 15: "DomainLocation", 16: "IsFrontFace", 17: "MSAACoverage", 18: "MSAASamplePosition", 19: "MSAASampleIndex", 20: "PatchNumVertices", 21: "OuterTessFactor", 22: "InsideTessFactor", 23: "ColourOutput", 24: "DepthOutput", 25: "DepthOutputGreaterEqual", 26: "DepthOutputLessEqual", } TEXTURE_DIM_MAP = { 0: "Unknown", 1: "Buffer", 2: "Texture1D", 3: "Texture1DArray", 4: "Texture2D", 5: "Texture2DArray", 6: "Texture2DMS", 7: "Texture2DMSArray", 8: "Texture3D", 9: "TextureCube", 10: "TextureCubeArray", } RESOURCE_USAGE_MAP = { 0: "None", 1: "VertexBuffer", 2: "IndexBuffer", 3: "VS_Constants", 4: "HS_Constants", 5: "DS_Constants", 6: "GS_Constants", 7: "PS_Constants", 8: "CS_Constants", 9: "All_Constants", 10: "StreamOut", 11: "IndirectArg", 16: "VS_Resource", 17: "HS_Resource", 18: "DS_Resource", 19: "GS_Resource", 20: "PS_Resource", 21: "CS_Resource", 22: "All_Resource", 32: "VS_RWResource", 33: "HS_RWResource", 34: "DS_RWResource", 35: "GS_RWResource", 36: "PS_RWResource", 37: "CS_RWResource", 38: "All_RWResource", 48: "InputTarget", 49: "ColorTarget", 50: "DepthStencilTarget", 64: "Clear", 65: "GenMips", 66: "Resolve", 67: "ResolveSrc", 68: "ResolveDst", 69: "Copy", 70: "CopySrc", 71: "CopyDst", 72: "Barrier", } def enum_str(value, mapping: dict, fallback_prefix: str = "") -> str: """Convert an enum value to a readable string, falling back to str().""" try: int_val = int(value) except (TypeError, ValueError): return str(value) return mapping.get(int_val, f"{fallback_prefix}{value}") def blend_formula( color_src: str, color_dst: str, color_op: str, alpha_src: str, alpha_dst: str, alpha_op: str, ) -> str: """Generate a human-readable blend formula string.""" def _op_str(op: str, a: str, b: str) -> str: if op == "Add": return f"{a} + {b}" elif op == "Subtract": return f"{a} - {b}" elif op == "RevSubtract": return f"{b} - {a}" elif op == "Min": return f"min({a}, {b})" elif op == "Max": return f"max({a}, {b})" return f"{a} {op} {b}" def _factor(f: str, channel: str) -> str: src, dst = f"src.{channel}", f"dst.{channel}" factor_map = { "Zero": "0", "One": "1", "SrcColor": "src.rgb", "InvSrcColor": "(1-src.rgb)", "DstColor": "dst.rgb", "InvDstColor": "(1-dst.rgb)", "SrcAlpha": "src.a", "InvSrcAlpha": "(1-src.a)", "DstAlpha": "dst.a", "InvDstAlpha": "(1-dst.a)", "SrcAlphaSat": "sat(src.a)", "BlendFactor": "factor", "InvBlendFactor": "(1-factor)", "Src1Color": "src1.rgb", "InvSrc1Color": "(1-src1.rgb)", "Src1Alpha": "src1.a", "InvSrc1Alpha": "(1-src1.a)", } return factor_map.get(f, f) c_src = _factor(color_src, "rgb") c_dst = _factor(color_dst, "rgb") color_expr = _op_str(color_op, f"{c_src}*src.rgb", f"{c_dst}*dst.rgb") a_src = _factor(alpha_src, "a") a_dst = _factor(alpha_dst, "a") alpha_expr = _op_str(alpha_op, f"{a_src}*src.a", f"{a_dst}*dst.a") return f"color: {color_expr} | alpha: {alpha_expr}" # ── Error helpers ── def make_error(message: str, code: str = "API_ERROR") -> dict: """Return a standardized error dict.""" return {"error": message, "code": code} # ── ActionFlags helpers ── _ACTION_FLAG_NAMES = None def _build_flag_names(): """Build a mapping of single-bit flag values to their names.""" names = {} for attr in dir(rd.ActionFlags): if attr.startswith("_"): continue val = getattr(rd.ActionFlags, attr) if isinstance(val, int) and val != 0 and (val & (val - 1)) == 0: names[val] = attr return names def flags_to_list(flags: int): """Convert an ActionFlags bitmask to a list of flag name strings.""" global _ACTION_FLAG_NAMES if _ACTION_FLAG_NAMES is None: _ACTION_FLAG_NAMES = _build_flag_names() result = [] for bit, name in _ACTION_FLAG_NAMES.items(): if flags & bit: result.append(name) return result # ── Serialization helpers ── def serialize_action( action, structured_file, depth: int = 0, max_depth: int = 2 ) -> dict: """Serialize an ActionDescription to a dict.""" result = { "event_id": action.eventId, "name": action.GetName(structured_file), "flags": flags_to_list(action.flags), "num_indices": action.numIndices, "num_instances": action.numInstances, } outputs = [] for o in action.outputs: rid = int(o) if rid != 0: outputs.append(str(o)) if outputs: result["outputs"] = outputs depth_id = int(action.depthOut) if depth_id != 0: result["depth_output"] = str(action.depthOut) if depth < max_depth and len(action.children) > 0: result["children"] = [ serialize_action(c, structured_file, depth + 1, max_depth) for c in action.children ] elif len(action.children) > 0: result["children_count"] = len(action.children) return result def serialize_action_detail(action, structured_file) -> dict: """Serialize a single ActionDescription with full detail (no depth limit on self, but no children expansion).""" result = { "event_id": action.eventId, "name": action.GetName(structured_file), "flags": flags_to_list(action.flags), "num_indices": action.numIndices, "num_instances": action.numInstances, "index_offset": action.indexOffset, "base_vertex": action.baseVertex, "vertex_offset": action.vertexOffset, "instance_offset": action.instanceOffset, "drawIndex": action.drawIndex, } outputs = [] for o in action.outputs: rid = int(o) if rid != 0: outputs.append(str(o)) result["outputs"] = outputs depth_id = int(action.depthOut) result["depth_output"] = str(action.depthOut) if depth_id != 0 else None if action.parent: result["parent_event_id"] = action.parent.eventId if action.previous: result["previous_event_id"] = action.previous.eventId if action.next: result["next_event_id"] = action.next.eventId result["children_count"] = len(action.children) return result def serialize_texture_desc(tex) -> dict: """Serialize a TextureDescription to a dict.""" return { "resource_id": str(tex.resourceId), "name": tex.name if hasattr(tex, "name") else "", "width": tex.width, "height": tex.height, "depth": tex.depth, "array_size": tex.arraysize, "mips": tex.mips, "format": str(tex.format.Name()), "dimension": enum_str(tex.dimension, TEXTURE_DIM_MAP, "Dim."), "msqual": tex.msQual, "mssamp": tex.msSamp, "creation_flags": tex.creationFlags, } def serialize_buffer_desc(buf) -> dict: """Serialize a BufferDescription to a dict.""" return { "resource_id": str(buf.resourceId), "name": buf.name if hasattr(buf, "name") else "", "length": buf.length, "creation_flags": buf.creationFlags, } def serialize_resource_desc(res) -> dict: """Serialize a ResourceDescription to a dict.""" try: name = res.name if hasattr(res, "name") else str(res.resourceId) except Exception: name = str(res.resourceId) return { "resource_id": str(res.resourceId), "name": name, "type": str(res.type), } def _get_var_type_accessor(var): """Determine the correct value accessor and type name for a ShaderVariable. Returns (accessor_attr, type_name) based on var.type. """ try: var_type = int(var.type) except Exception: return "f32v", "float" # RenderDoc VarType enum values: # Float=0, Double=1, Half=2, SInt=3, UInt=4, SShort=5, UShort=6, # SLong=7, ULong=8, SByte=9, UByte=10, Bool=11, Enum=12, # GPUPointer=13, ConstantBlock=14, Struct=15, Unknown=16 if var_type == 1: # Double return "f64v", "double" elif var_type in (3, 5, 7, 9): # SInt, SShort, SLong, SByte return "s32v", "int" elif var_type in (4, 6, 8, 10): # UInt, UShort, ULong, UByte return "u32v", "uint" elif var_type == 11: # Bool return "u32v", "bool" else: # Float, Half, and all others default to float return "f32v", "float" def serialize_shader_variable(var, max_depth: int = 10, depth: int = 0) -> dict: """Recursively serialize a ShaderVariable to a dict.""" result = {"name": var.name} if len(var.members) == 0: # Leaf variable - extract values using correct type accessor accessor, type_name = _get_var_type_accessor(var) result["type"] = type_name values = [] for r in range(var.rows): row_vals = [] for c in range(var.columns): row_vals.append(getattr(var.value, accessor)[r * var.columns + c]) values.append(row_vals) # Flatten single-row results if len(values) == 1: result["value"] = values[0] else: result["value"] = values result["rows"] = var.rows result["columns"] = var.columns elif depth < max_depth: result["members"] = [ serialize_shader_variable(m, max_depth, depth + 1) for m in var.members ] return result def serialize_usage_entry(usage) -> dict: """Serialize a single EventUsage entry.""" return { "event_id": usage.eventId, "usage": enum_str(usage.usage, RESOURCE_USAGE_MAP, "Usage."), } def serialize_sig_element(sig) -> dict: """Serialize a SigParameter (shader signature element).""" return { "var_name": sig.varName, "semantic_name": sig.semanticName, "semantic_index": sig.semanticIndex, "semantic_idx_name": sig.semanticIdxName, "var_type": enum_str(sig.varType, VAR_TYPE_MAP, "VarType."), "comp_count": sig.compCount, "system_value": enum_str(sig.systemValue, SYSTEM_VALUE_MAP, "SysValue."), "reg_index": sig.regIndex, } def to_json(obj: Any) -> str: """Serialize to compact JSON string.""" return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)