import struct from typing import List from typing import Tuple import renderdoc from . import util # Alias for convenience - we need to import as-is so types don't get confused rd = renderdoc def open_capture(filename="", cap: rd.CaptureFile=None, opts: rd.ReplayOptions=None): """ Opens a capture file and begins a replay. :param filename: The filename to open, or empty if cap is used. :param cap: The capture file to use, or ``None`` if a filename is given. :param opts: The replay options to use, or ``None`` to use the default options. :return: A replay controller for the capture :rtype: renderdoc.ReplayController """ if opts is None: opts = rd.ReplayOptions() # Open a capture file handle own_cap = False api = "Unknown" result = None controller = None if util.get_remote_server() is None: if cap is None: own_cap = True cap = rd.OpenCaptureFile() # Open a particular file result = cap.OpenFile(filename, '', None) # Make sure the file opened successfully if result != rd.ResultCode.Succeeded: cap.Shutdown() raise RuntimeError("Couldn't open '{}': {}".format(filename, str(result))) api = cap.DriverName() # Make sure we can replay if not cap.LocalReplaySupport(): cap.Shutdown() raise RuntimeError("{} capture cannot be replayed".format(api)) result, controller = cap.OpenCapture(opts, None) else: if not cap is None: raise ValueError("Cannot call analyse.open_capture() with capture handle for remote {}" .format(util.get_remote_server().remote)) result, controller = util.get_remote_server().OpenCapture(rd.RemoteServer.NoPreference, filename, opts, None) if result == rd.ResultCode.Succeeded: api = util.get_remote_server().remote.DriverName() if own_cap: cap.Shutdown() if result != rd.ResultCode.Succeeded: raise RuntimeError("Couldn't initialise replay for {}: {}".format(api, str(result))) return controller def fetch_indices(controller: rd.ReplayController, action: rd.ActionDescription, mesh: rd.MeshFormat, index_offset: int, first_index: int, num_indices: int): pipe = controller.GetPipelineState() restart_idx = pipe.GetRestartIndex() & ((1 << (mesh.indexByteStride*8)) - 1) restart_enabled = pipe.IsRestartEnabled() # If we have an index buffer if mesh.indexResourceId != rd.ResourceId.Null(): offset = mesh.indexByteStride*(first_index + index_offset) avail_bytes = mesh.indexByteSize if avail_bytes > offset: avail_bytes = avail_bytes - offset else: avail_bytes = 0 read_bytes = min([avail_bytes, mesh.indexByteStride*num_indices]) # Fetch the data if read_bytes > 0: ibdata = controller.GetBufferData(mesh.indexResourceId, mesh.indexByteOffset + offset, read_bytes) else: ibdata = bytes() # Get the character for the width of index index_fmt = 'B' if mesh.indexByteStride == 2: index_fmt = 'H' elif mesh.indexByteStride == 4: index_fmt = 'I' avail_indices = int(len(ibdata) / mesh.indexByteStride) # Duplicate the format by the number of indices index_fmt = '=' + str(min([avail_indices, num_indices])) + index_fmt # Unpack all the indices indices = struct.unpack_from(index_fmt, ibdata) extra = [] if avail_indices < num_indices: extra = [None] * (num_indices - avail_indices) # Apply the baseVertex offset return [i if restart_enabled and i == restart_idx else i + mesh.baseVertex for i in indices] + extra else: # With no index buffer, just generate a range return tuple(range(first_index, first_index + num_indices)) class MeshAttribute: mesh: rd.MeshFormat name: str def get_vsin_attrs(controller: rd.ReplayController, vertexOffset: int, index_mesh: rd.MeshFormat): pipe: rd.PipeState = controller.GetPipelineState() inputs: List[rd.VertexInputAttribute] = pipe.GetVertexInputs() attrs: List[MeshAttribute] = [] vbs: List[rd.BoundVBuffer] = pipe.GetVBuffers() for a in inputs: if not a.used: continue attr = MeshAttribute() attr.name = a.name attr.mesh = rd.MeshFormat(index_mesh) attr.mesh.vertexByteStride = vbs[a.vertexBuffer].byteStride attr.mesh.instStepRate = a.instanceRate attr.mesh.instanced = a.perInstance attr.mesh.vertexResourceId = vbs[a.vertexBuffer].resourceId offs = a.byteOffset + vertexOffset * attr.mesh.vertexByteStride attr.mesh.vertexByteOffset = vbs[a.vertexBuffer].byteOffset + offs attr.mesh.vertexByteSize = max([0, vbs[a.vertexBuffer].byteSize - offs]) attr.mesh.format = a.format attrs.append(attr) return attrs def get_postvs_attrs(controller: rd.ReplayController, mesh: rd.MeshFormat, data_stage: rd.MeshDataStage): pipe: rd.PipeState = controller.GetPipelineState() if data_stage == rd.MeshDataStage.VSOut: shader = pipe.GetShaderReflection(rd.ShaderStage.Vertex) elif data_stage == rd.MeshDataStage.TaskOut: raise RuntimeError("Use get_postts_attrs to get TaskOut attributes!") elif data_stage == rd.MeshDataStage.MeshOut: shader = pipe.GetShaderReflection(rd.ShaderStage.Mesh) else: shader = pipe.GetShaderReflection(rd.ShaderStage.Geometry) if shader is None: shader = pipe.GetShaderReflection(rd.ShaderStage.Domain) attrs: List[MeshAttribute] = [] posidx = 0 for sig in shader.outputSignature: attr = MeshAttribute() attr.mesh = rd.MeshFormat(mesh) if pipe.GetRasterizedStream() >= 0: if sig.stream != pipe.GetRasterizedStream(): continue else: if sig.stream != 0: continue # Ignore meshlet output indecies (they are not in postvs) if sig.systemValue == rd.ShaderBuiltin.OutputIndices: continue # Construct a resource format for this element attr.mesh.format = rd.ResourceFormat() attr.mesh.format.compByteWidth = rd.VarTypeByteSize(sig.varType) attr.mesh.format.compCount = sig.compCount attr.mesh.format.compType = rd.VarTypeCompType(sig.varType) attr.mesh.format.type = rd.ResourceFormatType.Regular attr.name = sig.semanticIdxName if sig.varName == '' else sig.varName if sig.systemValue == rd.ShaderBuiltin.Position: posidx = len(attrs) attrs.append(attr) # Shuffle the position element to the front if posidx > 0: pos = attrs[posidx] del attrs[posidx] attrs.insert(0, pos) accum_offset = 0 for i in range(0, len(attrs)): # Note that some APIs such as Vulkan will pad the size of the attribute here # while others will tightly pack fmt = attrs[i].mesh.format elem_size = (8 if fmt.compByteWidth > 4 else 4) alignment = elem_size if fmt.compCount == 2: alignment = elem_size * 2 elif fmt.compCount > 2: alignment = elem_size * 4 if pipe.HasAlignedPostVSData(data_stage) and (accum_offset % alignment) != 0: accum_offset += alignment - (accum_offset % alignment) attrs[i].mesh.vertexByteOffset += accum_offset accum_offset += elem_size * fmt.compCount return attrs # Unpack a tuple of the given format, from the data def unpack_data(fmt: rd.ResourceFormat, data: bytes, data_offset: int): # We don't handle 'special' formats - typically bit-packed such as 10:10:10:2 if fmt.Special(): raise RuntimeError("Packed formats are not supported!") format_chars = { # 012345678 rd.CompType.UInt: "xBHxIxxxQ", rd.CompType.SInt: "xbhxixxxq", rd.CompType.Float: "xxexfxxxd", # only 2, 4 and 8 are valid } # These types have identical decodes, but we might post-process them format_chars[rd.CompType.UNorm] = format_chars[rd.CompType.UInt] format_chars[rd.CompType.UScaled] = format_chars[rd.CompType.UInt] format_chars[rd.CompType.SNorm] = format_chars[rd.CompType.SInt] format_chars[rd.CompType.SScaled] = format_chars[rd.CompType.SInt] # We need to fetch compCount components vertex_format = '=' + str(fmt.compCount) + format_chars[fmt.compType][fmt.compByteWidth] if data_offset >= len(data): return None # Unpack the data try: value = struct.unpack_from(vertex_format, data, data_offset) except struct.error as ex: raise # If the format needs post-processing such as normalisation, do that now if fmt.compType == rd.CompType.UNorm: divisor = float((1 << (fmt.compByteWidth*8)) - 1) value = tuple(float(i) / divisor for i in value) elif fmt.compType == rd.CompType.SNorm: max_neg = -(1 << (fmt.compByteWidth*8 - 1)) divisor = -float(max_neg+1) value = tuple(-1.0 if (i == max_neg) else float(i / divisor) for i in value) elif fmt.compType == rd.CompType.UScaled or fmt.compType == rd.CompType.SScaled: value = tuple(float(i) for i in value) # If the format is BGRA, swap the two components if fmt.BGRAOrder(): value = tuple(value[i] for i in [2, 1, 0, 3]) return value def decode_mesh_data(controller: rd.ReplayController, indices: List[int], display_indices: List[int], attrs: List[MeshAttribute], instance: int = 0, indexOffset: int = 0): ret = [] buffer_ranges = {} for attr in attrs: begin = attr.mesh.vertexByteOffset end = min(begin + attr.mesh.vertexByteSize, 0xffffffffffffffff) # This could be more optimal if we figure out the lower/upper bounds of any attribute and only fetch the # data we need. For each referenced buffer, pick the attribute that references the largest range and fetch that if attr.mesh.vertexResourceId in buffer_ranges: buf_range = buffer_ranges[attr.mesh.vertexResourceId] if buf_range[0] < begin: begin = buf_range[0] if buf_range[1] > end: end = buf_range[1] buffer_ranges[attr.mesh.vertexResourceId] = (begin, end) buffer_data = {} for buf, buf_range in buffer_ranges.items(): buffer_data[buf] = controller.GetBufferData(buf, buf_range[0], buf_range[1] - buf_range[0]) # Calculate the strip restart index for this index width striprestart_index = None if controller.GetPipelineState().IsRestartEnabled() and attrs[0].mesh.indexResourceId != rd.ResourceId.Null(): striprestart_index = (controller.GetPipelineState().GetRestartIndex() & ((1 << (attrs[0].mesh.indexByteStride*8)) - 1)) for i,idx in enumerate(indices): vertex = {'vtx': i, 'idx': display_indices[i]} if striprestart_index is None or idx != striprestart_index: for attr in attrs: if idx is None: vertex[attr.name] = None continue offset = attr.mesh.vertexByteStride * idx if attr.mesh.instanced: offset = (attr.mesh.vertexByteStride + attr.mesh.vertexByteStride * int(instance / max(attr.mesh.instStepRate, 1))) vertex[attr.name] = unpack_data(attr.mesh.format, buffer_data[attr.mesh.vertexResourceId], attr.mesh.vertexByteOffset + offset - buffer_ranges[attr.mesh.vertexResourceId][0]) ret.append(vertex) return ret def str_vartype(t: rd.VarType) -> str: if t == rd.VarType.Bool: return "Bool" elif t == rd.VarType.ConstantBlock: return "ConstantBlock" elif t == rd.VarType.Double: return "Double" elif t == rd.VarType.Enum: return "Enum" elif t == rd.VarType.Float: return "Float" elif t == rd.VarType.GPUPointer: return "GPUPointer" elif t == rd.VarType.Half: return "Half" elif t == rd.VarType.ReadOnlyResource: return "ReadOnlyResource" elif t == rd.VarType.ReadWriteResource: return "ReadWriteResource" elif t == rd.VarType.Sampler: return "Sampler" elif t == rd.VarType.SByte: return "SByte" elif t == rd.VarType.SInt: return "SInt" elif t == rd.VarType.SLong: return "SLong" elif t == rd.VarType.SShort: return "SShort" elif t == rd.VarType.Struct: return "Struct" elif t == rd.VarType.UByte: return "UByte" elif t == rd.VarType.UInt: return "UInt" elif t == rd.VarType.ULong: return "ULong" elif t == rd.VarType.Unknown: return "Unknown" elif t == rd.VarType.UShort: return "UShort" return "???" def shadervariable_equal(a: rd.ShaderVariable, b : rd.ShaderVariable) -> Tuple[bool, str]: difference = "" if a.rows != b.rows: difference = f"Rows differ: {a.rows} != {b.rows}" return (False, difference) if a.columns != b.columns: difference = f"Columns differ: {a.columns} != {b.columns}" return (False, difference) if a.name != b.name: difference = f"Names differ: {a.name} != {b.name}" return (False, difference) if a.type != b.type: difference = f"Types differ: {str_vartype(a.type)} != {str_vartype(b.type)}" return (False, difference) if a.flags != b.flags: difference = f"Flags differ: {a.flags} != {b.flags}" return (False, difference) if len(a.members) != len(b.members): difference = f"Member count differs: {len(a.members)} != {len(b.members)}" return (False, difference) for i in range(a.rows * a.columns): if a.type == rd.VarType.UByte or a.type == rd.VarType.SByte: if a.value.u8v[i] != b.value.u8v[i]: return (False, f"Values differ at index {i}: {a.value.u8v[i]} != {b.value.u8v[i]}") elif a.type == rd.VarType.Half or a.type == rd.VarType.UShort or a.type == rd.VarType.SShort: if a.value.u16v[i] != b.value.u16v[i]: return (False, f"Values differ at index {i}: {a.value.u16v[i]} != {b.value.u16v[i]}") elif a.type == rd.VarType.Float or a.type == rd.VarType.UInt or a.type == rd.VarType.SInt or a.type == rd.VarType.Bool or a.type == rd.VarType.Enum: if a.value.u32v[i] != b.value.u32v[i]: return (False, f"Values differ at index {i}: {a.value.u32v[i]} != {b.value.u32v[i]}") elif a.type == rd.VarType.Double or a.type == rd.VarType.ULong or a.type == rd.VarType.SLong or a.type == rd.VarType.GPUPointer: if a.value.u64v[i] != b.value.u64v[i]: return (False, f"Values differ at index {i}: {a.value.u64v[i]} != {b.value.u64v[i]}") else: if a.value.u64v[i] != b.value.u64v[i]: return (False, f"Values differ at index {i}: {a.value.u64v[i]} != {b.value.u64v[i]}") for m in range(len(a.members)): (ret, diff) = shadervariable_equal(a.members[m], b.members[m]) if not ret: difference = f"Member[{m}] differs {diff}" return (False, difference) return (True, "")