mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-04 09:00:44 +00:00
428 lines
16 KiB
Python
428 lines
16 KiB
Python
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, "") |