Tidy up python documentation and add examples

This commit is contained in:
baldurk
2018-06-15 19:43:16 +01:00
parent 45c3c27923
commit 11091f1e54
32 changed files with 1860 additions and 16 deletions
@@ -0,0 +1,282 @@
import renderdoc as rd
# We'll need the struct data to read out of bytes objects
import struct
# We base our data on a MeshFormat, but we add some properties
class MeshData(rd.MeshFormat):
indexOffset = 0
name = ''
# Recursively search for the drawcall with the most vertices
def biggestDraw(prevBiggest, d):
ret = prevBiggest
if ret == None or d.numIndices > ret.numIndices:
ret = d
for c in d.children:
biggest = biggestDraw(ret, c)
if biggest.numIndices > ret.numIndices:
ret = biggest
return ret
# Unpack a tuple of the given format, from the data
def unpackData(fmt, data):
# 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!")
formatChars = {}
# 012345678
formatChars[rd.CompType.UInt] = "xBHxIxxxL"
formatChars[rd.CompType.SInt] = "xbhxixxxl"
formatChars[rd.CompType.Float] = "xxexfxxxd" # only 2, 4 and 8 are valid
# These types have identical decodes, but we might post-process them
formatChars[rd.CompType.UNorm] = formatChars[rd.CompType.UInt]
formatChars[rd.CompType.UScaled] = formatChars[rd.CompType.UInt]
formatChars[rd.CompType.SNorm] = formatChars[rd.CompType.SInt]
formatChars[rd.CompType.SScaled] = formatChars[rd.CompType.SInt]
formatChars[rd.CompType.Double] = formatChars[rd.CompType.Float]
# We need to fetch compCount components
vertexFormat = str(fmt.compCount) + formatChars[fmt.compType][fmt.compByteWidth]
# Unpack the data
value = struct.unpack_from(vertexFormat, data, 0)
# If the format needs post-processing such as normalisation, do that now
if fmt.compType == rd.CompType.UNorm:
divisor = float((1 << fmt.compByteWidth) - 1)
value = tuple(float(value[i]) / divisor for i in value)
elif fmt.compType == rd.CompType.SNorm:
maxNeg = -(1 << (fmt.compByteWidth - 1))
divisor = float(-(maxNeg-1))
value = tuple((float(value[i]) if (value[i] == maxNeg) else (float(value[i]) / divisor)) 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
# Get a list of MeshData objects describing the vertex inputs at this draw
def getMeshInputs(controller, draw):
# This work is API-specific, but the APIs have a lot of similarities.
# Here we implement support for D3D11 since it's relatively simple
state = controller.GetD3D11PipelineState()
# Get the index & vertex buffers, and fixed vertex inputs
ib = state.inputAssembly.indexBuffer
vbs = state.inputAssembly.vertexBuffers
attrs = state.inputAssembly.layouts
meshInputs = []
for attr in attrs:
# We don't handle instance attributes
if attr.perInstance:
raise RuntimeError("Instanced properties are not supported!")
meshInput = MeshData()
meshInput.indexResourceId = ib.resourceId
meshInput.indexByteOffset = ib.byteOffset
meshInput.indexByteStride = draw.indexByteWidth
meshInput.baseVertex = draw.baseVertex
meshInput.indexOffset = draw.indexOffset
meshInput.numIndices = draw.numIndices
# If the draw doesn't use an index buffer, don't use it even if bound
if not (draw.flags & rd.DrawFlags.Indexed):
meshInput.indexResourceId = rd.ResourceId.Null()
# The total offset is the attribute offset from the base of the vertex
meshInput.vertexByteOffset = attr.byteOffset + vbs[attr.inputSlot].byteOffset
meshInput.format = attr.format
meshInput.vertexResourceId = vbs[attr.inputSlot].resourceId
meshInput.vertexByteStride = vbs[attr.inputSlot].byteStride
# We don't go into the details of semantic matching here, just use both as the name
meshInput.name = '%s%d' % (attr.semanticName, attr.semanticIndex)
meshInputs.append(meshInput)
return meshInputs
# Get a list of MeshData objects describing the vertex outputs at this draw
def getMeshOutputs(controller, postvs):
meshOutputs = []
posidx = 0
state = controller.GetD3D11PipelineState()
vs = state.vertexShader.reflection
# Repeat the process, but this time sourcing the data from postvs.
# Since these are outputs, we iterate over the list of outputs from the
# vertex shader's reflection data
for attr in vs.outputSignature:
# Copy most properties from the postvs struct
meshOutput = MeshData()
meshOutput.indexResourceId = postvs.indexResourceId
meshOutput.indexByteOffset = postvs.indexByteOffset
meshOutput.indexByteStride = postvs.indexByteStride
meshOutput.baseVertex = postvs.baseVertex
meshOutput.indexOffset = 0
meshOutput.numIndices = postvs.numIndices
# The total offset is the attribute offset from the base of the vertex,
# as calculated by the stride per index
meshOutput.vertexByteOffset = postvs.vertexByteOffset
meshOutput.vertexResourceId = postvs.vertexResourceId
meshOutput.vertexByteStride = postvs.vertexByteStride
# Construct a resource format for this element
meshOutput.format = rd.ResourceFormat()
meshOutput.format.compByteWidth = 8 if attr.compType == rd.CompType.Double else 4
meshOutput.format.compCount = attr.compCount
meshOutput.format.compType = attr.compType
meshOutput.format.type = rd.ResourceFormatType.Regular
meshOutput.name = attr.semanticIdxName if attr.varName == '' else attr.varName
if attr.systemValue == rd.ShaderBuiltin.Position:
posidx = len(meshOutputs)
meshOutputs.append(meshOutput)
# Shuffle the position element to the front
if posidx > 0:
pos = meshOutputs[posidx]
del meshOutputs[posidx]
meshOutputs.insert(0, pos)
accumOffset = 0
for i in range(0, len(meshOutputs)):
meshOutputs[i].vertexByteOffset = accumOffset
# Note that some APIs such as Vulkan will pad the size of the attribute here
# while others will tightly pack
fmt = meshOutputs[i].format
accumOffset += (8 if fmt.compType == rd.CompType.Double else 4) * fmt.compCount
return meshOutputs
def getIndices(controller, mesh):
# Get the character for the width of index
indexFormat = 'B'
if mesh.indexByteStride == 2:
indexFormat = 'H'
elif mesh.indexByteStride == 4:
indexFormat = 'I'
# Duplicate the format by the number of indices
indexFormat = str(mesh.numIndices) + indexFormat
# If we have an index buffer
if mesh.indexResourceId != rd.ResourceId.Null():
# Fetch the data
ibdata = controller.GetBufferData(mesh.indexResourceId, mesh.indexByteOffset, 0)
# Unpack all the indices, starting from the first index to fetch
offset = mesh.indexOffset * mesh.indexByteStride
indices = struct.unpack_from(indexFormat, ibdata, offset)
# Apply the baseVertex offset
return [i + mesh.baseVertex for i in indices]
else:
# With no index buffer, just generate a range
return tuple(range(vertexOffset, vertexOffset+mesh.numIndices))
def printMeshData(controller, meshData):
indices = getIndices(controller, meshData[0])
print("Mesh configuration:")
for attr in meshData:
print("\t%s:" % attr.name)
print("\t\t- vertex: %s / %d stride" % (attr.vertexResourceId, attr.vertexByteStride))
print("\t\t- format: %s x %s @ %d" % (attr.format.compType, attr.format.compCount, attr.vertexByteOffset))
# We'll decode the first three indices making up a triangle
for i in range(0, 3):
idx = indices[i]
print("Vertex %d is index %d:" % (i, idx))
for attr in meshData:
# This is the data we're reading from. This would be good to cache instead of
# re-fetching for every attribute for every index
offset = attr.vertexByteOffset + attr.vertexByteStride * idx
data = controller.GetBufferData(attr.vertexResourceId, offset, 0)
# Get the value from the data
value = unpackData(attr.format, data)
# We don't go into the details of semantic matching here, just print both
print("\tAttribute '%s': %s" % (attr.name, value))
def sampleCode(controller):
# Find the biggest drawcall in the whole capture
draw = None
for d in controller.GetDrawcalls():
draw = biggestDraw(draw, d)
# Move to that draw
controller.SetFrameEvent(draw.eventId, True)
print("Decoding mesh inputs at %d: %s\n\n" % (draw.eventId, draw.name))
# Calculate the mesh input configuration
meshInputs = getMeshInputs(controller, draw)
# Fetch and print the data from the mesh inputs
printMeshData(controller, meshInputs)
print("Decoding mesh outputs\n\n")
# Fetch the postvs data
postvs = controller.GetPostVSData(0, rd.MeshDataStage.VSOut)
# Calcualte the mesh configuration from that
meshOutputs = getMeshOutputs(controller, postvs)
# Print it
printMeshData(controller, meshOutputs)
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
status = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't open file: " + str(status))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
status,controller = cap.OpenCapture(None)
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(status))
return (cap, controller)
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
cap,controller = loadCapture('test.rdc')
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
@@ -0,0 +1,310 @@
Decoding Mesh Data
==================
In this example we will fetch the geometry inputs to and outputs from a vertex shader. While this sample does not handle all possible edge cases, it is more complex than most others.
First we gather the API state that describes the vertex input data. In this example we will use the D3D11 pipeline state, but the same can be done for other APIs:
.. highlight:: python
.. code:: python
# This work is API-specific, but the APIs have a lot of similarities.
# Here we implement support for D3D11 since it's relatively simple
state = controller.GetD3D11PipelineState()
# Get the index & vertex buffers, and fixed vertex inputs
ib = state.inputAssembly.indexBuffer
vbs = state.inputAssembly.vertexBuffers
attrs = state.inputAssembly.layouts
We iterate over every attribute defined, and create an object that describes where to source it from, based on :py:class:`~renderdoc.MeshFormat` - since that is the format returned by :py:meth:`~renderdoc.ReplayController.GetPostVSData` this allows us to re-use code.
In the object we pass both the indices (which does not vary per attribute in our case) as well as the data for the vertex buffer the attribute comes from.
.. highlight:: python
.. code:: python
for attr in attrs:
# We don't handle instance attributes
if attr.perInstance:
raise RuntimeError("Instanced properties are not supported!")
meshInput = MeshData()
meshInput.indexResourceId = ib.resourceId
meshInput.indexByteOffset = ib.byteOffset
meshInput.indexByteStride = draw.indexByteWidth
meshInput.baseVertex = draw.baseVertex
meshInput.indexOffset = draw.indexOffset
meshInput.numIndices = draw.numIndices
# If the draw doesn't use an index buffer, don't use it even if bound
if not (draw.flags & rd.DrawFlags.Indexed):
meshInput.indexResourceId = rd.ResourceId.Null()
# The total offset is the attribute offset from the base of the vertex
meshInput.vertexByteOffset = attr.byteOffset + vbs[attr.inputSlot].byteOffset
meshInput.format = attr.format
meshInput.vertexResourceId = vbs[attr.inputSlot].resourceId
meshInput.vertexByteStride = vbs[attr.inputSlot].byteStride
# We don't go into the details of semantic matching here, just use both as the name
meshInput.name = '%s%d' % (attr.semanticName, attr.semanticIndex)
meshInputs.append(meshInput)
Next we fetch the index data using :py:meth:`~renderdoc.ReplayController.GetBufferData`, applying any offsets that might be present, and decode it using python's ``struct`` module. If we're not using index buffers, then we just generate a range of indices from the first vertex up to the number of indices.
.. highlight:: python
.. code:: python
def getIndices(controller, mesh):
# Get the character for the width of index
indexFormat = 'B'
if mesh.indexByteStride == 2:
indexFormat = 'H'
elif mesh.indexByteStride == 4:
indexFormat = 'I'
# Duplicate the format by the number of indices
indexFormat = str(mesh.numIndices) + indexFormat
# If we have an index buffer
if mesh.indexResourceId != rd.ResourceId.Null():
# Fetch the data
ibdata = controller.GetBufferData(mesh.indexResourceId, mesh.indexByteOffset, 0)
# Unpack all the indices, starting from the first index to fetch
offset = mesh.indexOffset * mesh.indexByteStride
indices = struct.unpack_from(indexFormat, ibdata, offset)
# Apply the baseVertex offset
return [i + mesh.baseVertex for i in indices]
else:
# With no index buffer, just generate a range
return tuple(range(vertexOffset, vertexOffset+mesh.numIndices))
To begin with, we define a helper that will read a given variable out of a ``bytes`` object, using a :py:class:`~renderdoc.ResourceFormat` do define the size and format of the data.
We only handle simple regular formatted types, rather than bit-packed types, to simplify the code. As a shortcut, we use a hash of strings, where the hash key is the component type, and then the character index in the string is the byte width. This gives us the ``struct.unpack`` character to decode one component of the variable, then we prepend the number of components to fetch.
For normalised formats - :py:attr:`~renderdoc.CompType.UNorm` and :py:attr:`~renderdoc.CompType.SNorm` - we also divide the resulting integer value to get the final floating point value used.
.. highlight:: python
.. code:: python
# Unpack a tuple of the given format, from the data
def unpackData(fmt, data):
# 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!")
formatChars = {}
# 012345678
formatChars[rd.CompType.UInt] = "xBHxIxxxL"
formatChars[rd.CompType.SInt] = "xbhxixxxl"
formatChars[rd.CompType.Float] = "xxexfxxxd" # only 2, 4 and 8 are valid
# These types have identical decodes, but we might post-process them
formatChars[rd.CompType.UNorm] = formatChars[rd.CompType.UInt]
formatChars[rd.CompType.UScaled] = formatChars[rd.CompType.UInt]
formatChars[rd.CompType.SNorm] = formatChars[rd.CompType.SInt]
formatChars[rd.CompType.SScaled] = formatChars[rd.CompType.SInt]
formatChars[rd.CompType.Double] = formatChars[rd.CompType.Float]
# We need to fetch compCount components
vertexFormat = str(fmt.compCount) + formatChars[fmt.compType][fmt.compByteWidth]
# Unpack the data
value = struct.unpack_from(vertexFormat, data, 0)
# If the format needs post-processing such as normalisation, do that now
if fmt.compType == rd.CompType.UNorm:
divisor = float((1 << fmt.compByteWidth) - 1)
value = tuple(float(value[i]) / divisor for i in value)
elif fmt.compType == rd.CompType.SNorm:
maxNeg = -(1 << (fmt.compByteWidth - 1))
divisor = float(-(maxNeg-1))
value = tuple((float(value[i]) if (value[i] == maxNeg) else (float(value[i]) / divisor)) 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
Finally with that helper defined we can iterate over each attribute for the first three indices:
.. highlight:: python
.. code:: python
indices = getIndices(controller, meshData[0])
# We'll decode the first three indices making up a triangle
for i in range(0, 3):
idx = indices[i]
print("Vertex %d is index %d:" % (i, idx))
for attr in meshData:
Using the index, we can fetch the right vertex data for each vertex's attribute using :py:meth:`~renderdoc.ReplayController.GetBufferData` again. This simplified approach is very wasteful since we re-fetch the same vertex data for each vertex buffer over and over. A more realistic sample would cache the vertex data:
.. highlight:: python
.. code:: python
# This is the data we're reading from. This would be good to cache instead of
# re-fetching for every attribute for every index
offset = attr.vertexByteOffset + attr.vertexByteStride * idx
data = controller.GetBufferData(attr.vertexResourceId, offset, 0)
# Get the value from the data
value = unpackData(attr.format, data)
# We don't go into the details of semantic matching here, just print both
print("\tAttribute '%s': %s" % (attr.name, str(value)))
For the vertex outputs, we do something very similar but instead of fetching the attributes from state bindings, we look at the shader reflection data of the vertex. Similarly instead of fetching the vertex byte data from bound vertex buffers, we call :py:meth:`~renderdoc.ReplayController.GetPostVSData` to fetch it from the analysis.
In the case of vertex outputs there is no explicit offset available, so we calculate our own offsets. Note that for some APIs like Vulkan the outputs are not necessarily tightly packed, so padding calculations may be necessary.
The position output is also treated specially - it always appears first, regardless of the actual order of the outputs. We solve this by noting which output is the builtin position output, and shuffling it to the start of the array.
.. highlight:: python
.. code:: python
posidx = 0
state = controller.GetD3D11PipelineState()
vs = state.vertexShader.reflection
# Repeat the process, but this time sourcing the data from postvs.
# Since these are outputs, we iterate over the list of outputs from the
# vertex shader's reflection data
for attr in vs.outputSignature:
# Copy most properties from the postvs struct
meshOutput = MeshData()
meshOutput.indexResourceId = postvs.indexResourceId
meshOutput.indexByteOffset = postvs.indexByteOffset
meshOutput.indexByteStride = postvs.indexByteStride
meshOutput.baseVertex = postvs.baseVertex
meshOutput.indexOffset = 0
meshOutput.numIndices = postvs.numIndices
# The total offset is the attribute offset from the base of the vertex,
# as calculated by the stride per index
meshOutput.vertexByteOffset = postvs.vertexByteOffset
meshOutput.vertexResourceId = postvs.vertexResourceId
meshOutput.vertexByteStride = postvs.vertexByteStride
# Construct a resource format for this element
meshOutput.format = rd.ResourceFormat()
meshOutput.format.compByteWidth = 8 if attr.compType == rd.CompType.Double else 4
meshOutput.format.compCount = attr.compCount
meshOutput.format.compType = attr.compType
meshOutput.format.type = rd.ResourceFormatType.Regular
meshOutput.name = attr.semanticIdxName if attr.varName == '' else attr.varName
if attr.systemValue == rd.ShaderBuiltin.Position:
posidx = len(meshOutputs)
meshOutputs.append(meshOutput)
# Shuffle the position element to the front
if posidx > 0:
pos = meshOutputs[posidx]
del meshOutputs[posidx]
meshOutputs.insert(0, pos)
accumOffset = 0
for i in range(0, len(meshOutputs)):
meshOutputs[i].vertexByteOffset = accumOffset
# Note that some APIs such as Vulkan will pad the size of the attribute here
# while others will tightly pack
fmt = meshOutputs[i].format
accumOffset += (8 if fmt.compType == rd.CompType.Double else 4) * fmt.compCount
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <decode_mesh.py>`.
.. literalinclude:: decode_mesh.py
Sample output:
.. sourcecode:: text
Decoding mesh inputs at 69: DrawIndexed(5580)
Mesh configuration:
POSITION0:
- vertex: <ResourceId 142> / 44 stride
- format: CompType.Float x 3 @ 0
TANGENT0:
- vertex: <ResourceId 142> / 44 stride
- format: CompType.Float x 3 @ 12
NORMAL0:
- vertex: <ResourceId 142> / 44 stride
- format: CompType.Float x 3 @ 24
TEXCOORD0:
- vertex: <ResourceId 142> / 44 stride
- format: CompType.Float x 2 @ 36
Vertex 0 is index 0:
Attribute 'POSITION0': (1.0, -1.5, 0.0)
Attribute 'TANGENT0': (-0.0, 0.0, 1.0)
Attribute 'NORMAL0': (0.9701425433158875, 0.24253533780574799, 0.0)
Attribute 'TEXCOORD0': (0.0, 1.0)
Vertex 1 is index 31:
Attribute 'POSITION0': (0.9750000238418579, -1.399999976158142, 0.0)
Attribute 'TANGENT0': (-0.0, 0.0, 1.0)
Attribute 'NORMAL0': (0.9701424241065979, 0.24253588914871216, 0.0)
Attribute 'TEXCOORD0': (0.0, 0.9666666388511658)
Vertex 2 is index 32:
Attribute 'POSITION0': (0.9536939859390259, -1.399999976158142, 0.20271390676498413)
Attribute 'TANGENT0': (-0.20791170001029968, 0.0, 0.9781476259231567)
Attribute 'NORMAL0': (0.9489423036575317, 0.24253617227077484, 0.20170393586158752)
Attribute 'TEXCOORD0': (0.03333333507180214, 0.9666666388511658)
Decoding mesh outputs
Mesh configuration:
SV_POSITION:
- vertex: <ResourceId 1000000000000000237> / 68 stride
- format: CompType.Float x 4 @ 0
POSITION:
- vertex: <ResourceId 1000000000000000237> / 68 stride
- format: CompType.Float x 4 @ 16
TEXCOORD:
- vertex: <ResourceId 1000000000000000237> / 68 stride
- format: CompType.Float x 2 @ 32
TANGENT:
- vertex: <ResourceId 1000000000000000237> / 68 stride
- format: CompType.Float x 3 @ 40
NORMAL:
- vertex: <ResourceId 1000000000000000237> / 68 stride
- format: CompType.Float x 4 @ 52
Vertex 0 is index 0:
Attribute 'SV_POSITION': (-6.269223690032959, -4.345583915710449, -2.4250497817993164, -2.0)
Attribute 'POSITION': (-4.0, 0.0, -12.0, 1.0)
Attribute 'TEXCOORD': (0.0, 1.0)
Attribute 'TANGENT': (-5.0, 1.5, -11.0)
Attribute 'NORMAL': (0.9701425433158875, 0.24253533780574799, 0.0, 0.0)
Vertex 1 is index 31:
Attribute 'SV_POSITION': (-6.308406352996826, -4.104162216186523, -2.4250497817993164, -2.0)
Attribute 'POSITION': (-4.025000095367432, 0.10000002384185791, -12.0, 1.0)
Attribute 'TEXCOORD': (0.0, 0.9666666388511658)
Attribute 'TANGENT': (-5.0, 1.5, -11.0)
Attribute 'NORMAL': (0.9701424241065979, 0.24253588914871216, 0.0, 0.0)
Vertex 2 is index 32:
Attribute 'SV_POSITION': (-6.341799736022949, -4.104162216186523, -2.221970558166504, -1.797286033630371)
Attribute 'POSITION': (-4.046306133270264, 0.10000002384185791, -11.797286033630371, 1.0)
Attribute 'TEXCOORD': (0.03333333507180214, 0.9666666388511658)
Attribute 'TANGENT': (-5.207911491394043, 1.5, -11.021852493286133)
Attribute 'NORMAL': (0.9489423036575317, 0.24253617227077484, 0.20170393586158752, 0.0)
@@ -0,0 +1,118 @@
import renderdoc as rd
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
status = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't open file: " + str(status))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
status,controller = cap.OpenCapture(None)
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(status))
return controller
if 'pyrenderdoc' in globals():
raise RuntimeError("This sample should not be run within the RenderDoc UI")
else:
controller = loadCapture('test.rdc')
# Use tkinter to create windows
import tkinter
# Create a simple window
window = tkinter.Tk()
window.geometry("1280x720")
# Create renderdoc windowing data.
winsystems = [rd.WindowingSystem(i) for i in controller.GetSupportedWindowSystems()]
# Pass window system specific data here, See:
# - renderdoc.CreateWin32WindowingData
# - renderdoc.CreateXlibWindowingData
# - renderdoc.CreateXCBWindowingData
# This example code works on windows as that's simple to integrate with tkinter
if not rd.WindowingSystem.Win32 in winsystems:
raise RuntimeError("Example requires Win32 windowing system: " + str(winsystems))
windata = rd.CreateWin32WindowingData(int(window.frame(), 16))
# Create a texture output on the window
out = controller.CreateOutput(windata, rd.ReplayOutputType.Texture)
# Fetch the list of textures
textures = controller.GetTextures()
# Fetch the list of drawcalls
draws = controller.GetDrawcalls()
# Function to look up the texture descriptor for a given resourceId
def getTexture(texid):
global textures
for tex in textures:
if tex.resourceId == texid:
return tex
return None
# Our paint function will be called ever 33ms, to display the output
def paint():
global out, window
out.Display()
window.after(33, paint)
# Start on the first drawcall
curdraw = 0
# The advance function will be called every 150ms, to move to the next draw
def advance():
global out, window, curdraw
# Move to the current drawcall
controller.SetFrameEvent(draws[curdraw].eventId, False)
# Initialise a default TextureDisplay object
disp = rd.TextureDisplay()
# Set the first colour output as the texture to display
disp.resourceId = draws[curdraw].outputs[0]
# Get the details of this texture
texDetails = getTexture(disp.resourceId)
# Calculate the scale required in width and height
widthScale = window.winfo_width() / texDetails.width
heightScale = window.winfo_height() / texDetails.height
# Use the lower scale to fit the texture on the window
disp.scale = min(widthScale, heightScale)
# Update the texture display
out.SetTextureDisplay(disp)
# Set the next drawcall
curdraw = (curdraw + 1) % len(draws)
window.after(150, advance)
# Start the callbacks
advance()
paint()
# Start the main window loop
window.mainloop()
controller.Shutdown()
cap.Shutdown()
@@ -0,0 +1,138 @@
Display texture in window
=========================
In this example we will open a window and iterate through the capture on a loop, displaying the first colour output target on it.
.. note::
This is intended for use with the python module directly, as the UI already has a texture viewer panel to do this with much more control. The principle is the same though and it can be useful reference of how to iterate over a capture.
To create a window we use tkinter, since it is provided with the Python distribution.
.. highlight:: python
.. code:: python
# Use tkinter to create windows
import tkinter
# Create a simple window
window = tkinter.Tk()
window.geometry("1280x720")
Next we need to determine which windowing systems the RenderDoc implementation supports, and create a :py:class:`~renderdoc.WindowingData` object for the window we want to render to. For the purposes of this example we will look for Win32 since it's the simplest to set up - needing only a window handle that we can get from tkinter easily. XCB/XLib require a display connection, which would be possible to get from another library such as Qt.
Once we have the :py:class:`~renderdoc.WindowingData`, we can create a :py:class:`~renderdoc.ReplayOutput` using :py:meth:`~renderdoc.ReplayController.CreateOutput`.
.. highlight:: python
.. code:: python
# Create renderdoc windowing data.
winsystems = [rd.WindowingSystem(i) for i in controller.GetSupportedWindowSystems()]
# Pass window system specific data here, See:
# - renderdoc.CreateWin32WindowingData
# - renderdoc.CreateXlibWindowingData
# - renderdoc.CreateXCBWindowingData
# This example code works on windows as that's simple to integrate with tkinter
if not rd.WindowingSystem.Win32 in winsystems:
raise RuntimeError("Example requires Win32 windowing system: " + str(winsystems))
windata = rd.CreateWin32WindowingData(int(window.frame(), 16))
# Create a texture output on the window
out = controller.CreateOutput(windata, rd.ReplayOutputType.Texture)
In order to iterate over all drawcalls we need some global state first from :py:meth:`~renderdoc.ReplayController.GetTextures` and :py:meth:`~renderdoc.ReplayController.GetDrawcalls`, and we'll also define a helper function to fetch a particular texture by resourceId, so that we can easily look up the details for a texture.
.. highlight:: python
.. code:: python
# Fetch the list of textures
textures = controller.GetTextures()
# Fetch the list of drawcalls
draws = controller.GetDrawcalls()
# Function to look up the texture descriptor for a given resourceId
def getTexture(texid):
global textures
for tex in textures:
if tex.resourceId == texid:
return tex
return None
We now define two callback functions - ``paint`` and ``advance``. ``paint`` will be called every 33ms, it will display the output with the latest state using :py:meth:`~renderdoc.ReplayOutput.Display`. ``advance`` changes the current state to reflect a new drawcall.
.. highlight:: python
.. code:: python
# Our paint function will be called ever 33ms, to display the output
def paint():
global out, window
out.Display()
window.after(33, paint)
Within ``advance`` we do a few things. First we move the current event to the current drawcall's ``eventId``, using :py:meth:`~renderdoc.ReplayController.SetFrameEvent`. Then we set up the texture display configuration with :py:meth:`~renderdoc.ReplayOutput.SetTextureDisplay`, to point to the first colour output at that drawcall.
When we update to a new texture, we fetch its details using our earlier ``getTexture`` and calculate a scale that keeps the texture fully visible on screen.
Finally we move to the next drawcall in the list for the next time ``advance`` is called.
.. highlight:: python
.. code:: python
# Start on the first drawcall
curdraw = 0
# The advance function will be called every 150ms, to move to the next draw
def advance():
global out, window, curdraw
# Move to the current drawcall
controller.SetFrameEvent(draws[curdraw].eventId, False)
# Initialise a default TextureDisplay object
disp = rd.TextureDisplay()
# Set the first colour output as the texture to display
disp.resourceId = draws[curdraw].outputs[0]
# Get the details of this texture
texDetails = getTexture(disp.resourceId)
# Calculate the scale required in width and height
widthScale = window.winfo_width() / texDetails.width
heightScale = window.winfo_height() / texDetails.height
# Use the lower scale to fit the texture on the window
disp.scale = min(widthScale, heightScale)
# Update the texture display
out.SetTextureDisplay(disp)
# Set the next drawcall
curdraw = (curdraw + 1) % len(draws)
window.after(150, advance)
Once we have the callbacks defined, we call them once to initialise the display and set up the repeated callbacks, and start the tkinter main window loop.
.. highlight:: python
.. code:: python
# Start the callbacks
advance()
paint()
# Start the main window loop
window.mainloop()
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <display_window.py>`.
.. literalinclude:: display_window.py
@@ -0,0 +1,79 @@
import renderdoc as rd
def printVar(v, indent = ''):
print(indent + v.name + ":")
if len(v.members) == 0:
valstr = ""
for r in range(0, v.rows):
valstr += indent + ' '
for c in range(0, v.columns):
valstr += '%.3f ' % v.value.fv[r*v.columns + c]
if r < v.rows-1:
valstr += "\n"
print(valstr)
for v in v.members:
printVar(v, indent + ' ')
def sampleCode(controller):
print("Available disassembly formats:")
targets = controller.GetDisassemblyTargets()
for disasm in targets:
print(" - " + disasm)
target = targets[0]
# For some APIs, it might be relevant to set the PSO id or entry point name
pipe = rd.ResourceId.Null()
entry = "main"
# Get the pixel shader's reflection object
ps = controller.GetD3D11PipelineState().pixelShader
print("Pixel shader:")
print(controller.DisassembleShader(pipe, ps.reflection, target))
cbufferVars = controller.GetCBufferVariableContents(ps.resourceId, entry, 0, ps.constantBuffers[0].resourceId, 0)
for v in cbufferVars:
printVar(v)
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
status = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't open file: " + str(status))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
status,controller = cap.OpenCapture(None)
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(status))
return (cap, controller)
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
cap,controller = loadCapture('test.rdc')
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
@@ -0,0 +1,203 @@
Fetch Shader details
====================
In this example we will fetch the disassembly for a shader and a set of constant values.
When disassembling a shader there may be more than one possible representation available, so we first enumerate the formats that are available using :py:meth:`~renderdoc.ReplayController.GetDisassemblyTargets` before selecting a target to disassemble to. The first target is always a reasonable default, and there will be at least one:
.. highlight:: python
.. code:: python
print("Available disassembly formats:")
targets = controller.GetDisassemblyTargets()
for disasm in targets:
print(" - " + disasm)
target = targets[0]
Next we fetch any ancillary data that might be needed to disassemble - this varies by API depending on whether it supports multiple entry points per shader, or has a concept of pipeline state objects that are used together with a shader to disassemble.
For the purposes of this example we use D3D11 which does not require either, so we set some default values and fetch the shader from the pipeline state. Finally we fetch the disassembled shader string with :py:meth:`~renderdoc.ReplayController.DisassembleShader` and print it:
.. highlight:: python
.. code:: python
# For some APIs, it might be relevant to set the PSO id or entry point name
pipe = rd.ResourceId.Null()
entry = "main"
# Get the pixel shader's reflection object
ps = controller.GetD3D11PipelineState().pixelShader
print("Pixel shader:")
print(controller.DisassembleShader(pipe, ps.reflection, target))
Now we want to display the constants bound to this shader. Shader bindings is an area that diverges quite a lot between the APIs, and RenderDoc's abstraction over this is detailed in :py:class:`~renderdoc.BindPointMap`. For now, we'll simply select the first constant buffer in this shader and fetch the constants for it with :py:meth:`~renderdoc.ReplayController.GetCBufferVariableContents`.
.. highlight:: python
.. code:: python
cbufferVars = controller.GetCBufferVariableContents(ps.resourceId, entry, 0, ps.constantBuffers[0].resourceId, 0)
Since constants can contain structs of other constants, we want to define a recursive function that will iterate over a constant and print it along with its value. We want to handle both vectors and matrices so we need to iterate over both rows and columns for each variable.
.. highlight:: python
.. code:: python
def printVar(v, indent = ''):
print(indent + v.name + ":")
if len(v.members) == 0:
valstr = ""
for r in range(0, v.rows):
valstr += indent + ' '
for c in range(0, v.columns):
valstr += '%.3f ' % v.value.fv[r*v.columns + c]
if r < v.rows-1:
valstr += "\n"
print(valstr)
for v in v.members:
printVar(v, indent + ' ')
Finally, we iterate over the constants that we fetched earlier calling the function for each.
.. highlight:: python
.. code:: python
for v in cbufferVars:
printVar(v)
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <fetch_shader.py>`.
.. literalinclude:: fetch_shader.py
Sample output:
.. sourcecode:: text
Available disassembly formats:
- DXBC
- AMD GCN ISA
Pixel shader:
Shader hash 9dd8337a-c75dd787-1fa0f07e-5f39f955
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[8], immediateIndexed
dcl_sampler gDepthSam (s0), mode_default
dcl_resource_texture2d (float,float,float,float) gDepthMap (t0)
dcl_resource_texture2d (float,float,float,float) gGBufferMap (t1)
dcl_input_ps linear v1.xyz
dcl_input_ps linear v2.xyw
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_temps 6
0: nop
1: mov r0.xyz, v2.xywx
2: div r0.xy, r0.xyxx, r0.zzzz
3: mul r0.xy, r0.xyxx, l(0.500000, -0.500000, 0.000000, 0.000000)
4: add r0.xy, r0.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000)
5: mov r0.xy, r0.xyxx
6: sample_indexable(texture2d)(float,float,float,float) r0.z, r0.xyxx, gDepthMap.yzxw, gDepthSam
7: mov r0.z, r0.z
8: nop
9: mov r0.z, r0.z
10: mov r0.z, -r0.z
11: add r0.z, r0.z, l(1.001801)
12: div r0.z, l(0.421448), r0.z
13: mov r0.z, r0.z
14: dp3 r0.w, v1.xyzx, v1.xyzx
15: rsq r0.w, r0.w
16: mul r1.xyz, r0.wwww, v1.xyzx
17: div r0.z, r0.z, r1.z
18: mul r2.xyz, r0.zzzz, r1.xyzx
19: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, r0.xyxx, gGBufferMap.xyzw, gDepthSam
20: dp3 r1.w, r0.xyzx, r0.xyzx
21: rsq r1.w, r1.w
22: mul r0.xyz, r0.xyzx, r1.wwww
23: mov r3.xyz, gLightPosV.xyzx
24: mov r4.xyz, -r2.xyzx
25: add r4.xyz, r3.xyzx, r4.xyzx
26: dp3 r1.w, r4.xyzx, r4.xyzx
27: rsq r1.w, r1.w
28: mul r4.xyz, r1.wwww, r4.xyzx
29: dp3 r1.w, r0.xyzx, r4.xyzx
30: max r5.x, r1.w, l(0)
31: nop
32: mov r1.xyz, -r1.xyzx
33: add r1.xyz, r1.xyzx, r4.xyzx
34: dp3 r1.w, r1.xyzx, r1.xyzx
35: rsq r1.w, r1.w
36: mul r1.xyz, r1.wwww, r1.xyzx
37: mov r0.xyz, r0.xyzx
38: mul r0.w, r0.w, l(64.000000)
39: dp3 r0.x, r1.xyzx, r0.xyzx
40: max r0.x, r0.x, l(0)
41: log r0.x, r0.x
42: mul r0.x, r0.x, r0.w
43: exp r5.y, r0.x
44: mov r5.y, r5.y
45: nop
46: mov r3.xyz, r3.xyzx
47: mov r2.xyz, r2.xyzx
48: mov r0.xyz, gLight.att.xyzx
49: mov r1.xyz, -r2.xyzx
50: add r1.xyz, r1.xyzx, r3.xyzx
51: dp3 r0.w, r1.xyzx, r1.xyzx
52: sqrt r0.w, r0.w
53: mul r0.y, r0.y, r0.w
54: add r0.x, r0.y, r0.x
55: mul r0.y, r0.w, r0.w
56: mul r0.y, r0.z, r0.y
57: add r0.x, r0.y, r0.x
58: div r0.x, l(1.000000), r0.x
59: mov r0.x, r0.x
60: mul r0.xy, r5.xyxx, r0.xxxx
61: max r0.xy, r0.xyxx, l(0, 0, 0, 0)
62: mul o0.xyzw, r0.xxxx, gLight.diffuse.xyzw
63: mul o1.xyzw, r0.yyyy, gLight.diffuse.xyzw
64: ret
gLight:
pos:
-2.022 2.000 -3.694
dir:
0.000 0.000 0.000
ambient:
0.300 0.300 0.300 1.000
diffuse:
0.300 1.000 0.600 1.000
spec:
0.500 0.500 0.500 1.000
att:
0.000 0.200 0.100
spotPower:
0.000
range:
3.000
gLightPosV:
-2.022 0.200 6.306 -107374176.000
gLigthDirES:
-0.298 -0.596 -0.745
gWorldViewProj:
1.567 0.000 0.000 0.000
0.000 2.414 0.000 0.000
0.000 0.000 1.002 1.000
-3.169 0.483 5.896 6.306
gWorldView:
1.000 0.000 0.000 0.000
0.000 1.000 0.000 0.000
0.000 0.000 1.000 0.000
-2.022 0.200 6.306 1.000
@@ -0,0 +1,33 @@
renderdoc Examples
==================
Here we have some examples of the lower level renderdoc API. Some may be only relevant when using the module directly, but most are general and can be followed either when running in python or in the UI.
In order to help with this, the examples are organised such that most code is written within a function that accepts the replay controller, and the code from :doc:`../renderdoc_intro` runs only when standalone and not within the UI:
.. note::
While some of the samples will work within the UI, because they directly access the lower level API and skip the UI's API, it may cause some state inconsistencies. See :doc:`../qrenderdoc/index` for examples using the UI API directly.
.. highlight:: python
.. code:: python
def sampleCode(controller):
print("Here we can use the replay controller")
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
cap,controller = loadCapture('test.rdc')
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
.. toctree::
iter_draws
fetch_shader
save_texture
decode_mesh
display_window
@@ -0,0 +1,96 @@
import renderdoc as rd
# Set up a hash for storing draw information by event
draws = {}
# Define a recursive function for iterating over draws
def iterDraw(d, indent = ''):
global draws
# Print this drawcall
print('%s%d: %s' % (indent, d.eventId, d.name))
# Save the draw by eventId for use later
draws[d.eventId] = d
# Iterate over the draw's children
for d in d.children:
iterDraw(d, indent + ' ')
def sampleCode(controller):
global draws
# Iterate over all of the root drawcalls
for d in controller.GetDrawcalls():
iterDraw(d)
# Start iterating from the first real draw as a child of markers
draw = controller.GetDrawcalls()[0]
while len(draw.children) > 0:
draw = draw.children[0]
# Counter for which pass we're in
passnum = 0
# Counter for how many draws are in the pass
passcontents = 0
# Whether we've started seeing draws in the pass - i.e. we're past any
# starting clear calls that may be batched together
inpass = False
print("Pass #0 starts with %d: %s" % (draw.eventId, draw.name))
while draw != None:
# When we encounter a clear
if draw.flags & rd.DrawFlags.Clear:
if inpass:
print("Pass #%d contained %d draws" % (passnum, passcontents))
passnum += 1
print("Pass #%d starts with %d: %s" % (passnum, draw.eventId, draw.name))
passcontents = 0
inpass = False
else:
passcontents += 1
inpass = True
# Advance to the next drawcall
if not draw.next in draws:
break
draw = draws[draw.next]
if inpass:
print("Pass #%d contained %d draws" % (passnum, passcontents))
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
status = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't open file: " + str(status))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
status,controller = cap.OpenCapture(None)
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(status))
return controller
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
cap,controller = loadCapture('test.rdc')
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
@@ -0,0 +1,105 @@
Iterate drawcall tree
=====================
In this example we will show how to iterate over drawcalls.
The drawcalls returned from :py:meth:`~renderdoc.ReplayController.GetDrawcalls` are draws or marker regions at the root level - with no parent marker region. There are multiple ways to iterate through the list of draws.
The first way illustrated in this sample is to walk the tree using :py:attr:`~renderdoc.DrawcallDescription.children`, which contains the list of child draws at any point in the tree. There is also :py:attr:`~renderdoc.DrawcallDescription.parent` which contains the index of the parent drawcall.
The second is to use :py:attr:`~renderdoc.DrawcallDescription.previous` and :py:attr:`~renderdoc.DrawcallDescription.next`, which contain the indices of the previous and next draw respectively in a linear fashion, regardless of nesting depth.
In the example we use this iteration to determine the number of passes, using the drawcall flags to denote the start of each pass by a starting clear call.
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <iter_draws.py>`.
.. literalinclude:: iter_draws.py
Sample output:
.. sourcecode:: text
1: Scene
2: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 1.000000)
3: ClearDepthStencilView(D=1.000000, S=00)
9: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 0.000000)
10: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 0.000000)
11: ClearDepthStencilView(D=1.000000, S=00)
13: GBuffer
28: DrawIndexed(4800)
43: DrawIndexed(36)
56: DrawIndexed(5220)
69: DrawIndexed(5580)
82: DrawIndexed(5580)
95: DrawIndexed(5580)
108: DrawIndexed(5580)
121: DrawIndexed(5580)
134: DrawIndexed(5580)
147: DrawIndexed(5580)
160: DrawIndexed(5580)
173: DrawIndexed(5580)
186: DrawIndexed(5580)
199: DrawIndexed(5220)
212: DrawIndexed(5220)
225: DrawIndexed(5220)
238: DrawIndexed(5220)
251: DrawIndexed(5220)
264: DrawIndexed(5220)
277: DrawIndexed(5220)
290: DrawIndexed(5220)
303: DrawIndexed(5220)
316: DrawIndexed(5220)
319: ClearDepthStencilView(D=1.000000, S=00)
321: Shadowmap
333: DrawIndexed(4800)
345: DrawIndexed(36)
355: DrawIndexed(5220)
365: DrawIndexed(5580)
375: DrawIndexed(5580)
385: DrawIndexed(5580)
395: DrawIndexed(5580)
405: DrawIndexed(5580)
415: DrawIndexed(5580)
425: DrawIndexed(5580)
435: DrawIndexed(5580)
445: DrawIndexed(5580)
455: DrawIndexed(5580)
465: DrawIndexed(5220)
475: DrawIndexed(5220)
485: DrawIndexed(5220)
495: DrawIndexed(5220)
505: DrawIndexed(5220)
515: DrawIndexed(5220)
525: DrawIndexed(5220)
535: DrawIndexed(5220)
545: DrawIndexed(5220)
555: DrawIndexed(5220)
558: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 1.000000)
559: ClearDepthStencilView(D=1.000000, S=00)
561: Lighting
563: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 0.000000)
564: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 0.000000)
580: DrawIndexed(36)
597: DrawIndexed(36)
614: DrawIndexed(36)
617: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 1.000000)
618: ClearDepthStencilView(D=1.000000, S=00)
620: Shading
630: Draw(6)
645: DrawIndexed(960)
652: API Calls
655: End of Frame
Pass #0 starts with 2: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 1.000000)
Pass #0 contained 24 draws
Pass #1 starts with 319: ClearDepthStencilView(D=1.000000, S=00)
Pass #1 contained 24 draws
Pass #2 starts with 558: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 1.000000)
Pass #2 contained 3 draws
Pass #3 starts with 617: ClearRenderTargetView(0.000000, 0.000000, 0.000000, 1.000000)
Pass #3 contained 4 draws
@@ -0,0 +1,101 @@
import renderdoc as rd
# Recursively search for the drawcall with the most vertices
def biggestDraw(prevBiggest, d):
ret = prevBiggest
if ret == None or d.numIndices > ret.numIndices:
ret = d
for c in d.children:
biggest = biggestDraw(ret, c)
if biggest.numIndices > ret.numIndices:
ret = biggest
return ret
def sampleCode(controller):
# Find the biggest drawcall in the whole capture
draw = None
for d in controller.GetDrawcalls():
draw = biggestDraw(draw, d)
# Move to that draw
controller.SetFrameEvent(draw.eventId, True)
texsave = rd.TextureSave()
# Select the first color output
texsave.resourceId = draw.outputs[0]
if texsave.resourceId == rd.ResourceId.Null():
return
filename = str(int(texsave.resourceId))
print("Saving images of %s at %d: %s" % (filename, draw.eventId, draw.name))
# Save different types of texture
# Blend alpha to a checkerboard pattern for formats without alpha support
texsave.alpha = rd.AlphaMapping.BlendToCheckerboard
# Most formats can only display a single image per file, so we select the
# first mip and first slice
texsave.mip = 0
texsave.slice.sliceIndex = 0
texsave.destType = rd.FileType.JPG
controller.SaveTexture(texsave, filename + ".jpg")
texsave.destType = rd.FileType.HDR
controller.SaveTexture(texsave, filename + ".hdr")
# For formats with an alpha channel, preserve it
texsave.alpha = rd.AlphaMapping.Preserve
texsave.destType = rd.FileType.PNG
controller.SaveTexture(texsave, filename + ".png")
# DDS textures can save multiple mips and array slices, so instead
# of the default behaviour of saving mip 0 and slice 0, we set -1
# which saves *all* mips and slices
texsave.mip = -1
texsave.slice.sliceIndex = -1
texsave.destType = rd.FileType.DDS
controller.SaveTexture(texsave, filename + ".dds")
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
status = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't open file: " + str(status))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
status,controller = cap.OpenCapture(None)
if status != rd.ReplayStatus.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(status))
return (cap, controller)
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
cap,controller = loadCapture('test.rdc')
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
@@ -0,0 +1,25 @@
Save a texture to disk
======================
In this example we will find a particular drawcall, and save the color output to disk as an image file.
To begin with, so that we have an interesting drawcall selected we iterate over the list of draws finding the drawcall with the highest vertex count. For more on how to iterate through a capture's list of drawcalls, see :doc:`iter_draws`.
Once we have set the drawcall we want as the current event, we can configure the texture save operation. To do this we create a :py:class:`~renderdoc.TextureSave` object. The properties of the object determine how the texture will be mapped to an image file to be saved to disk.
At minimum you need to select a file format, and we'll try a few - :py:attr:`~renderdoc.FileType.JPG`, :py:attr:`~renderdoc.FileType.HDR`, :py:attr:`~renderdoc.FileType.PNG`, and :py:attr:`~renderdoc.FileType.DDS`.
For :py:attr:`~renderdoc.FileType.JPG` and :py:attr:`~renderdoc.FileType.HDR`, alpha is not supported so we choose to blend to a checkerboard pattern in RGB, so the alpha is 'visible'. You can also choose other alpha operations. For the other formats they support alpha natively so we preserve it.
:py:attr:`~renderdoc.FileType.DDS` is the only format that supports mip levels and array slices, so we choose to keep all of these in the output file instead of selecting only one. It is also possible to map array slices into a grid to display an array texture in a single-image format.
:py:attr:`~renderdoc.FileType.DDS` will also support the exact format that the texture is in, rather than encoding it to a different precision.
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <save_texture.py>`.
.. literalinclude:: save_texture.py