Remove old python documentation, ready for replacement

This commit is contained in:
baldurk committed 2026-08-13 21:05:11 +01:00
1 parent 2654224fb5
commit 11b83f2272
30 files changed
+2 -3185

No files matched your search

-54
View File
@@ -1,54 +0,0 @@
Basic Interfaces
================
This document explains some common interfaces and their relationship, which can be useful as a primer to understand where to get started, as well as for reference to look back on from later examples.
Replay Basics
-------------
ReplayController
^^^^^^^^^^^^^^^^
The primary interface for accessing the low level of RenderDoc's replay analysis is :py:class:`~renderdoc.ReplayController`.
From this interface, information about the capture can be gathered, using e.g. :py:meth:`~renderdoc.ReplayController.GetRootActions` to return the list of root-level actions in the frame, or :py:meth:`~renderdoc.ReplayController.GetResources` to return a list of all resources in the capture.
Some methods like the two above return information which is global and does not vary across the frame. Most functions however return information relative to the current point in the frame.
During RenderDoc's replay, you can imagine a cursor that moves back and forth between the start and end of the frame. All requests for information that varies - such as texture and buffer contents, pipeline state, and other information will be relative to the current event.
Every function call within a frame is assigned an ascending ``eventId``, from ``1`` up to as many events as are in the frame. Within the action list returned by :py:meth:`~renderdoc.ReplayController.GetRootActions`, each action contains a list of events in :py:attr:`~renderdoc.ActionDescription.events`. These contain all of the ``eventId`` that immediately preceded the action. The details of the function call can be found by using :py:attr:`~renderdoc.APIEvent.chunkIndex` as an index into the structured data returned from :py:meth:`~qrenderdoc.CaptureContext.GetStructuredFile`. The structured data contains the function name and the complete set of parameters passed to it, with their values.
To change the current active event and move the cursor, you can call :py:meth:`~renderdoc.ReplayController.SetFrameEvent`. This will move the replay to represent the current state immediately after the given event has executed.
At this point you can use :py:meth:`~renderdoc.ReplayController.GetBufferData` and :py:meth:`~renderdoc.ReplayController.GetTextureData` to obtain the contents of a buffer or texture respectively. The pipeline state can be accessed via ``Get*PipelineState`` for each API - to determine the current capture's pipeline type you can fetch the API properties from :py:meth:`~renderdoc.ReplayController.GetAPIProperties`.
There is also an API-agnostic pipeline abstraction to return information that is the same across APIs. Using :py:meth:`~qrenderdoc.CaptureContext.CurPipelineState` returns a :py:class:`~renderdoc.PipeState` which has accessors for fetching the current vertex buffers, shaders, and colour outputs. This allows you to write generic code that will work on any API that RenderDoc supports. The API-specific pipelines are still available through ``Get*PipelineState``.
For more examples of how to fetch data, see the concrete examples below.
ReplayOutput
^^^^^^^^^^^^
While :py:class:`~renderdoc.ReplayController` provides methods for obtaining data directly, it doesn't provide any functionality for displaying to a window. For this the :py:class:`~renderdoc.ReplayOutput` class allows you to bind to a window and configure the output.
First you need to gather the platform-specific windowing information in a :py:class:`~renderdoc.WindowingData`. This class is opaque to python, but you can create it using helper functions such as :py:func:`~renderdoc.CreateWin32WindowingData` and :py:func:`~renderdoc.CreateXlibWindowingData`. The parameters to these are platform specific, and are typically accepted as integers where they refer to a windowing handle.
To then create an output for a window, :py:meth:`~renderdoc.ReplayController.CreateOutput` can create different types of outputs.
Once created, you can configure the output with :py:meth:`~renderdoc.ReplayOutput.SetMeshDisplay` and :py:meth:`~renderdoc.ReplayOutput.SetTextureDisplay` to update the configuration, and then call :py:meth:`~renderdoc.ReplayOutput.Display` to display on screen.
.. _qrenderdoc-python-basics:
RenderDoc UI Basics
-------------------
The RenderDoc UI provides a number of useful abstractions over the lower level API, which can be convenient when developing scripts. In addition it gives access to the different panels to allow limited control over them. The ``pyrenderdoc`` global is available to all scripts running within the RenderDoc UI, and it provides access to all of these things.
Each single-instance panel such as the :py:class:`~qrenderdoc.TextureViewer` or :py:class:`~qrenderdoc.PipelineStateViewer` has accessors within the :py:class:`~qrenderdoc.CaptureContext`.
Functions such as :py:meth:`~qrenderdoc.CaptureContext.GetTextureViewer` will return a valid handle to the texture viewer, but if the texture viewer was closed then although it will be created it will *not* be immediately visible. You need to call :py:meth:`~qrenderdoc.CaptureContext.ShowTextureViewer` first which will bring the texture viewer to the front and make sure it is visible and docked if it wasn't already.
You can also create new instances of windows such as buffer or shader viewers using :py:meth:`~qrenderdoc.CaptureContext.ViewBuffer` or :py:meth:`~qrenderdoc.CaptureContext.ViewShader`.
The :py:class:`~qrenderdoc.CaptureContext` interface also provides useful utility functions such as :py:meth:`~qrenderdoc.CaptureContext.GetTexture` or :py:meth:`~qrenderdoc.CaptureContext.GetAction` to look up objects by id instead of needing your own caching and lookup from the lists returned by the lower level interface.
@@ -1,7 +0,0 @@
qrenderdoc examples
===================
These examples are only relevant to the scripting available in the UI.
.. toctree::
show_buffer
@@ -1,23 +0,0 @@
filename = "test.rdc"
formatter = "float3 pos; half norms[16]; uint flags;"
pyrenderdoc.LoadCapture(filename, renderdoc.ReplayOptions(), filename, False, True)
mybuf = renderdoc.ResourceId.Null()
for buf in pyrenderdoc.GetBuffers():
print("buf %s is %s" % (buf.resourceId, pyrenderdoc.GetResourceName(buf.resourceId)))
# here put your actual selection criteria - i.e. look for a particular name
if pyrenderdoc.GetResourceName(buf.resourceId) == "dataBuffer":
mybuf = buf.resourceId
break
print("selected %s" % pyrenderdoc.GetResourceName(mybuf))
if mybuf != renderdoc.ResourceId.Null():
# Open a new buffer viewer for this buffer, with the given format
bufview = pyrenderdoc.ViewBuffer(0, 0, mybuf, formatter)
# Show the buffer viewer on the main tool area
pyrenderdoc.AddDockWindow(bufview.Widget(), qrenderdoc.DockReference.MainToolArea, None)
@@ -1,50 +0,0 @@
Display buffer with format
==========================
This example shows an easy way to automate a repro case. It could be run as a command line argument when starting the UI, to avoid repetitive steps.
First the code opens a specified file, although this step could be omitted if the desired capture is already open.
.. highlight:: python
.. code:: python
filename = "test.rdc"
pyrenderdoc.LoadCapture(filename, renderdoc.ReplayOptions(), filename, False, True)
Next we iterate through the list of buffers to find the one we want. The selection criteria are up to you, in this case we look at the name provided and identify the buffer by that, however it could also be a particular size, or the buffer bound at a given event.
.. highlight:: python
.. code:: python
mybuf = renderdoc.ResourceId.Null()
for buf in pyrenderdoc.GetBuffers():
print("buf %s is %s" % (buf.resourceId, pyrenderdoc.GetResourceName(buf.resourceId)))
# here put your actual selection criteria - i.e. look for a particular name
if pyrenderdoc.GetResourceName(buf.resourceId) == "dataBuffer":
mybuf = buf.resourceId
break
print("selected %s" % pyrenderdoc.GetResourceName(mybuf))
Once we've identified the buffer we want to view, we create a buffer viewer and display it on the main tool area.
.. highlight:: python
.. code:: python
# Open a new buffer viewer for this buffer, with the given format
bufview = pyrenderdoc.ViewBuffer(0, 0, mybuf, formatter)
# Show the buffer viewer on the main tool area
pyrenderdoc.AddDockWindow(bufview.Widget(), qrenderdoc.DockReference.MainToolArea, None)
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <show_buffer.py>`.
.. literalinclude:: show_buffer.py
@@ -1,52 +0,0 @@
Getting Started (RenderDoc UI)
==============================
.. note::
This document is aimed at users getting started with scripting within the RenderDoc UI.
The module used here (``qrenderdoc``) is not available as a stand-alone module.
When working within the RenderDoc UI, the ``renderdoc`` and ``qrenderdoc`` modules are implicitly imported when any script runs. In addition to this, an additional global ``pyrenderdoc`` is always available. It is an instance of :py:class:`~qrenderdoc.CaptureContext` and it represents the interface into the UI interface.
Loading a Capture
-----------------
Unlike :doc:`when using the base module directly <renderdoc_intro>`, within the UI it is strongly recommended that you load captures using the UI interfaces itself rather than doing it entirely within python code, otherwise there is a risk of conflict between the two loaded captures in the same program.
To load a capture programmatically, we can use the ``pyrenderdoc`` global variable and call :py:meth:`~qrenderdoc.CaptureContext.LoadCapture`. When loading a local capture some of the parameters are redundant - we don't need to specify a different "actual" file vs the loaded file, and it is not a temporary handle. These parameters are primarily used by the UI itself when loading captures from remote hosts or immediately after they are generated while they are still stored temporarily on disk and the user needs to be prompted to save or delete them on close.
.. highlight:: python
.. code:: python
filename = 'test.rdc'
# Load a file, with the same 'original' name, that's not temporary, and is local
pyrenderdoc.LoadCapture(filename, renderdoc.ReplayOptions(), filename, False, True)
This will close any capture that is already loaded, but to just close an open capture you can use :py:meth:`~qrenderdoc.CaptureContext.CloseCapture`.
As part of opening the capture, RenderDoc will begin replay automatically and populate the various panels and internal data structures for easy access. It will also handle prompting the user if any errors happen or if replay isn't supported.
Once the capture is opened, all of the RenderDoc UI accessible data is immediately available and can be accessed directly, see :ref:`qrenderdoc-python-basics`.
Accessing Capture Analysis
--------------------------
To access the :py:class:`~renderdoc.ReplayController` and any related core interfaces, a little bit of extra work is required. Within the UI, the replay work happens on a separate thread to prevent long-running tasks from causing the UI to become unresponsive. That means the :py:class:`~renderdoc.ReplayController` is not immediately available, but is provided to a callback on the right thread.
To invoke onto the right thread, you can use :py:meth:`~qrenderdoc.ReplayManager.BlockInvoke` and pass it a callback that will be called with a single parameter - the :py:class:`~renderdoc.ReplayController` instance for the currently open capture.
.. warning::
There is another invoke function :py:meth:`~qrenderdoc.ReplayManager.AsyncInvoke`, but due to Python's limited threading capability the callback can't be called while the script is executing, meaning this has limited use. For Python it is recommended to use :py:meth:`~qrenderdoc.ReplayManager.BlockInvoke`.
.. highlight:: python
.. code:: python
def myCallback(controller):
print("%d top-level actions" % len(controller.GetRootActions()))
pyrenderdoc.Replay().BlockInvoke(myCallback)
If there is no replay active, the callback will be silently dropped.
@@ -1,291 +0,0 @@
import sys
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
# 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]
# 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((2 ** (fmt.compByteWidth * 8)) - 1)
value = tuple(float(i) / divisor for i in value)
elif fmt.compType == rd.CompType.SNorm:
maxNeg = -float(2 ** (fmt.compByteWidth * 8)) / 2
divisor = float(-(maxNeg-1))
value = tuple((float(i) if (i == maxNeg) else (float(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):
state = controller.GetPipelineState()
# Get the index & vertex buffers, and fixed vertex inputs
ib = state.GetIBuffer()
vbs = state.GetVBuffers()
attrs = state.GetVertexInputs()
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 = ib.byteStride
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.ActionFlags.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.vertexBuffer].byteOffset + draw.vertexOffset * vbs[attr.vertexBuffer].byteStride
meshInput.format = attr.format
meshInput.vertexResourceId = vbs[attr.vertexBuffer].resourceId
meshInput.vertexByteStride = vbs[attr.vertexBuffer].byteStride
meshInput.name = attr.name
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
vs = controller.GetPipelineState().GetShaderReflection(rd.ShaderStage.Vertex)
# 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 = rd.VarTypeByteSize(attr.varType)
meshOutput.format.compCount = attr.compCount
meshOutput.format.compType = rd.VarTypeCompType(attr.varType)
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.compByteWidth > 4 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(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.GetRootActions():
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.GetName(controller.GetStructuredFile())))
# 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, 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
result = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
return (cap, controller)
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
if len(sys.argv) <= 1:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
cap,controller = loadCapture(sys.argv[1])
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
@@ -1,304 +0,0 @@
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 API abstraction :py:class:`~renderdoc.PipeState` so that this code works on a capture from any API:
.. highlight:: python
.. code:: python
state = controller.GetPipelineState()
# Get the index & vertex buffers, and fixed vertex inputs
ib = state.GetIBuffer()
vbs = state.GetVBuffers()
attrs = state.GetVertexInputs()
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 = ib.byteStride
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.ActionFlags.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.vertexBuffer].byteOffset + draw.vertexOffset * vbs[attr.vertexBuffer].byteStride
meshInput.format = attr.format
meshInput.vertexResourceId = vbs[attr.vertexBuffer].resourceId
meshInput.vertexByteStride = vbs[attr.vertexBuffer].byteStride
meshInput.name = attr.name
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(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]
# 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((2 ** (fmt.compByteWidth * 8)) - 1)
value = tuple(float(i) / divisor for i in value)
elif fmt.compType == rd.CompType.SNorm:
maxNeg = -float(2 ** (fmt.compByteWidth * 8)) / 2
divisor = float(-(maxNeg-1))
value = tuple((float(i) if (i == maxNeg) else (float(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
vs = controller.GetPipelineState().GetShaderReflection(rd.ShaderStage.Vertex)
# 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 = rd.VarTypeByteSize(attr.varType)
meshOutput.format.compCount = attr.compCount
meshOutput.format.compType = rd.VarTypeCompType(attr.varType)
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.compByteWidth > 4 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)
@@ -1,146 +0,0 @@
import sys
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
result = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
return cap,controller
if 'pyrenderdoc' in globals():
raise RuntimeError("This sample should not be run within the RenderDoc UI")
else:
if len(sys.argv) <= 1:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
cap,controller = loadCapture(sys.argv[1])
# 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 actions
actions = controller.GetRootActions()
# 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 action
curact = actions[0]
loopcount = 0
# The advance function will be called every 50ms, to move to the next action
def advance():
global out, window, curact, actions, loopcount
# Move to the current action
controller.SetFrameEvent(curact.eventId, False)
# Initialise a default TextureDisplay object
disp = rd.TextureDisplay()
# Set the first colour output as the texture to display
disp.resourceId = curact.outputs[0]
if disp.resourceId != rd.ResourceId.Null():
# 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 action
curact = curact.next
# If we have no next action, start again from the first
if curact is None:
loopcount = loopcount + 1
curact = actions[0]
# after 3 loops, quit
if loopcount == 3:
window.quit()
else:
window.after(50, advance)
# Start the callbacks
advance()
paint()
# Start the main window loop
window.mainloop()
controller.Shutdown()
cap.Shutdown()
if 'pyrenderdoc' not in globals():
rd.ShutdownReplay()
@@ -1,150 +0,0 @@
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 actions we need some global state first from :py:meth:`~renderdoc.ReplayController.GetTextures` and :py:meth:`~renderdoc.ReplayController.GetRootActions`, and we'll also define a helper function to fetch a particular texture by :class:`~renderdoc.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 actions
actions = controller.GetRootActions()
# 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 action.
.. 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 action'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 action.
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 action in the list for the next time ``advance`` is called.
.. highlight:: python
.. code:: python
# Start on the first action
curact = actions[0]
loopcount = 0
# The advance function will be called every 50ms, to move to the next action
def advance():
global out, window, curact, actions, loopcount
# Move to the current action
controller.SetFrameEvent(curact.eventId, False)
# Initialise a default TextureDisplay object
disp = rd.TextureDisplay()
# Set the first colour output as the texture to display
disp.resourceId = curact.outputs[0]
if disp.resourceId != rd.ResourceId.Null():
# 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 action
curact = curact.next
# If we have no next action, start again from the first
if curact is None:
loopcount = loopcount + 1
curact = actions[0]
# after 3 loops, quit
if loopcount == 3:
window.quit()
else:
window.after(50, 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
@@ -1,107 +0,0 @@
import sys
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
actions = {}
# Define a recursive function for iterating over actions
def iterDraw(d, indent = ''):
global actions
# save the action by eventId
actions[d.eventId] = d
# Iterate over the draw's children
for d in d.children:
iterDraw(d, indent + ' ')
def sampleCode(controller):
# Iterate over all of the root actions, so we have names for each
# eventId
for d in controller.GetRootActions():
iterDraw(d)
# Enumerate the available counters
counters = controller.EnumerateCounters()
if not (rd.GPUCounter.SamplesPassed in counters):
raise RuntimeError("Implementation doesn't support Samples Passed counter")
# Now we fetch the counter data, this is a good time to batch requests of as many
# counters as possible, the implementation handles any book keeping.
results = controller.FetchCounters([rd.GPUCounter.SamplesPassed])
# Get the description for the counter we want
samplesPassedDesc = controller.DescribeCounter(rd.GPUCounter.SamplesPassed)
# Describe each counter
for c in counters:
desc = controller.DescribeCounter(c)
print("Counter %d (%s):" % (c, desc.name))
print(" %s" % desc.description)
print(" Returns %d byte %s, representing %s" % (desc.resultByteWidth, desc.resultType, desc.unit))
# Look in the results for any draws with 0 samples written - this is an indication
# that if a lot of draws appear then culling could be better.
for r in results:
draw = actions[r.eventId]
# Only care about draws, not about clears and other misc events
if not (draw.flags & rd.ActionFlags.Drawcall):
continue
if samplesPassedDesc.resultByteWidth == 4:
val = r.value.u32
else:
val = r.value.u64
if val == 0:
print("EID %d '%s' had no samples pass depth/stencil test!" % (r.eventId, draw.GetName(controller.GetStructuredFile())))
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
result = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
return cap,controller
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
if len(sys.argv) <= 1:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
cap,controller = loadCapture(sys.argv[1])
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
@@ -1,119 +0,0 @@
Fetch GPU Counter Data
======================
In this example we will gather GPU counter data over a capture and find any actions that completely failed the depth/stencil test.
The first thing we do is enumerate a list of counters that the implementation supports using :py:meth:`~renderdoc.ReplayController.EnumerateCounters`. A few of these counters values are statically known - see :py:class:`~renderdoc.GPUCounter`. If you know which counter you want ahead of time you can continue straight away by calling :py:meth:`~renderdoc.ReplayController.FetchCounters` with a list of counters to sample from, or :py:meth:`~renderdoc.ReplayController.DescribeCounter` to obtain a :py:class:`~renderdoc.CounterDescription` of the counter itself:
.. highlight:: python
.. code:: python
# Enumerate the available counters
counters = controller.EnumerateCounters()
if not (rd.GPUCounter.SamplesPassed in counters):
raise RuntimeError("Implementation doesn't support Samples Passed counter")
# Now we fetch the counter data, this is a good time to batch requests of as many
# counters as possible, the implementation handles any book keeping.
results = controller.FetchCounters([rd.GPUCounter.SamplesPassed])
# Get the description for the counter we want
samplesPassedDesc = controller.DescribeCounter(rd.GPUCounter.SamplesPassed)
However we will also print all available counters. If the implementation supports vendor-specific counters they will be enumerated as well and we can print their descriptions.
.. highlight:: python
.. code:: python
# Describe each counter
for c in counters:
desc = controller.DescribeCounter(c)
print("Counter %d (%s):" % (c, desc.name))
print(" %s" % desc.description)
print(" Returns %d byte %s, representing %s" % (desc.resultByteWidth, desc.resultType, desc.unit))
Once we have the list of :py:class:`~renderdoc.CounterResult` from sampling the specified counters, each result returned is for one counter on one event. Since we only fetched one counter we can simply iterate over the results looking up the action for each. For actual draws (excluding clears and markers etc) we use the counter description to determine the data payload size, and get the value out. Interpreting this can either happen based on the description, or in our case we know that this counter returns a simple value we can check:
.. highlight:: python
.. code:: python
# Look in the results for any draws with 0 samples written - this is an indication
# that if a lot of draws appear then culling could be better.
for r in results:
draw = actions[r.eventId]
# Only care about draws, not about clears and other misc events
if not (draw.flags & rd.ActionFlags.Drawcall):
continue
if samplesPassedDesc.resultByteWidth == 4:
val = r.value.u32
else:
val = r.value.u64
if val == 0:
print("EID %d '%s' had no samples pass depth/stencil test!" % (r.eventId, draw.GetName(controller.GetStructuredFile())))
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <fetch_counters.py>`.
.. literalinclude:: fetch_counters.py
Sample output:
.. sourcecode:: text
Counter 1 (GPU Duration):
Time taken for this event on the GPU, as measured by delta between two GPU timestamps.
Returns 8 byte CompType.Float, representing CounterUnit.Seconds
Counter 2 (Input Vertices Read):
Number of vertices read by input assembler.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 3 (Input Primitives):
Number of primitives read by the input assembler.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 4 (GS Primitives):
Number of primitives output by a geometry shader.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 5 (Rasterizer Invocations):
Number of primitives that were sent to the rasterizer.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 6 (Rasterized Primitives):
Number of primitives that were rendered.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 7 (Samples Passed):
Number of samples that passed depth/stencil test.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
... which we will sample
Counter 8 (VS Invocations):
Number of times a vertex shader was invoked.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 9 (HS Invocations):
Number of times a hull shader was invoked.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 10 (DS Invocations):
Number of times a domain shader (or tesselation evaluation shader in OpenGL) was invoked.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 11 (GS Invocations):
Number of times a geometry shader was invoked.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 12 (PS Invocations):
Number of times a pixel shader was invoked.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
Counter 13 (CS Invocations):
Number of times a compute shader was invoked.
Returns 8 byte CompType.UInt, representing CounterUnit.Absolute
EID 69 'DrawIndexed()' had no samples pass depth/stencil test!
EID 82 'DrawIndexed()' had no samples pass depth/stencil test!
EID 95 'DrawIndexed()' had no samples pass depth/stencil test!
EID 108 'DrawIndexed()' had no samples pass depth/stencil test!
EID 199 'DrawIndexed()' had no samples pass depth/stencil test!
EID 212 'DrawIndexed()' had no samples pass depth/stencil test!
EID 225 'DrawIndexed()' had no samples pass depth/stencil test!
EID 238 'DrawIndexed()' had no samples pass depth/stencil test!
@@ -1,98 +0,0 @@
import sys
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
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.f32v[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(True)
for disasm in targets:
print(" - " + disasm)
target = targets[0]
state = controller.GetPipelineState()
# For some APIs, it might be relevant to set the PSO id or entry point name
pipe = state.GetGraphicsPipelineObject()
entry = state.GetShaderEntryPoint(rd.ShaderStage.Pixel)
# Get the pixel shader's reflection object
ps = state.GetShaderReflection(rd.ShaderStage.Pixel)
cb = state.GetConstantBlock(rd.ShaderStage.Pixel, 0, 0)
print("Pixel shader:")
print(controller.DisassembleShader(pipe, ps, target))
cbufferVars = controller.GetCBufferVariableContents(pipe, ps.resourceId, rd.ShaderStage.Pixel, entry, 0, cb.descriptor.resource, 0, 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
result = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
return (cap, controller)
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
if len(sys.argv) <= 1:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
cap,controller = loadCapture(sys.argv[1])
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
@@ -1,207 +0,0 @@
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. :py:meth:`~renderdoc.ReplayController.GetDisassemblyTargets` takes a parameter indicating whether the pipeline state object will be available - some disassembly targets require the full pipeline and are not available when disassembling only a shader in isolation:
.. highlight:: python
.. code:: python
print("Available disassembly formats:")
targets = controller.GetDisassemblyTargets(True)
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 the API abstraction :py:class:`~renderdoc.PipeState` so that this code works on a capture from any API, so we fetch the state bindings that we need. Finally we fetch the disassembled shader string with :py:meth:`~renderdoc.ReplayController.DisassembleShader` and print it:
.. highlight:: python
.. code:: python
state = controller.GetPipelineState()
# For some APIs, it might be relevant to set the PSO id or entry point name
pipe = state.GetGraphicsPipelineObject()
entry = state.GetShaderEntryPoint(rd.ShaderStage.Pixel)
# Get the pixel shader's reflection object
ps = state.GetShaderReflection(rd.ShaderStage.Pixel)
cb = state.GetConstantBlock(rd.ShaderStage.Pixel, 0, 0)
print("Pixel shader:")
print(controller.DisassembleShader(pipe, ps, 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 :ref:`more detail <descriptor-abstraction>`. 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(pipe, ps.resourceId, rd.ShaderStage.Pixel, entry, 0, cb.descriptor.resource, 0, 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.f32v[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
@@ -1,35 +0,0 @@
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_actions
fetch_shader
fetch_counters
save_texture
decode_mesh
display_window
remote_capture
@@ -1,101 +0,0 @@
import sys
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
# Define a recursive function for iterating over actions
def iterAction(d, indent = ''):
# Print this action
print('%s%d: %s' % (indent, d.eventId, d.GetName(controller.GetStructuredFile())))
# Iterate over the action's children
for d in d.children:
iterAction(d, indent + ' ')
def sampleCode(controller):
# Iterate over all of the root actions
for d in controller.GetRootActions():
iterAction(d)
# Start iterating from the first real action as a child of markers
action = controller.GetRootActions()[0]
while len(action.children) > 0:
action = action.children[0]
# Counter for which pass we're in
passnum = 0
# Counter for how many actions are in the pass
passcontents = 0
# Whether we've started seeing actions 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" % (action.eventId, action.GetName(controller.GetStructuredFile())))
while action != None:
# When we encounter a clear
if action.flags & rd.ActionFlags.Clear:
if inpass:
print("Pass #%d contained %d actions" % (passnum, passcontents))
passnum += 1
print("Pass #%d starts with %d: %s" % (passnum, action.eventId, action.GetName(controller.GetStructuredFile())))
passcontents = 0
inpass = False
else:
passcontents += 1
inpass = True
# Advance to the next action
action = action.next
if action is None:
break
if inpass:
print("Pass #%d contained %d actions" % (passnum, passcontents))
def loadCapture(filename):
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
result = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
return cap,controller
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
if len(sys.argv) <= 1:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
cap,controller = loadCapture(sys.argv[1])
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
@@ -1,161 +0,0 @@
Iterate Action tree
===================
In this example we will show how to iterate over actions.
The actions returned from :py:meth:`~renderdoc.ReplayController.GetRootActions` are actions or marker regions at the root level - with no parent marker region. There are multiple ways to iterate through the list of actions.
The first way illustrated in this sample is to walk the tree using :py:attr:`~renderdoc.ActionDescription.children`, which contains the list of child actions at any point in the tree. There is also :py:attr:`~renderdoc.ActionDescription.parent` which points to the parent action.
The second is to use :py:attr:`~renderdoc.ActionDescription.previousAction` and :py:attr:`~renderdoc.ActionDescription.nextAction`, which point to the previous and next action respectively in a linear fashion, regardless of nesting depth.
In the example we use this iteration to determine the number of passes, using the action 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_actions.py>`.
.. literalinclude:: iter_actions.py
Sample output:
.. sourcecode:: text
1: Scene
2: ID3D11DeviceContext::ClearRenderTargetView()
3: ID3D11DeviceContext::ClearDepthStencilView()
9: ID3D11DeviceContext::ClearRenderTargetView()
10: ID3D11DeviceContext::ClearRenderTargetView()
11: ID3D11DeviceContext::ClearDepthStencilView()
13: GBuffer
25: Floor
28: ID3D11DeviceContext::DrawIndexed()
29: empty label
30: ID3DUserDefinedAnnotation::EndEvent()
40: Base
43: ID3D11DeviceContext::DrawIndexed()
53: Center sphere
56: ID3D11DeviceContext::DrawIndexed()
66: Cone
69: ID3D11DeviceContext::DrawIndexed()
79: Cone
82: ID3D11DeviceContext::DrawIndexed()
92: Cone
95: ID3D11DeviceContext::DrawIndexed()
105: Cone
108: ID3D11DeviceContext::DrawIndexed()
118: Cone
121: ID3D11DeviceContext::DrawIndexed()
131: Cone
134: ID3D11DeviceContext::DrawIndexed()
144: Cone
147: ID3D11DeviceContext::DrawIndexed()
157: Cone
160: ID3D11DeviceContext::DrawIndexed()
170: Cone
173: ID3D11DeviceContext::DrawIndexed()
183: Cone
186: ID3D11DeviceContext::DrawIndexed()
196: Sphere
199: ID3D11DeviceContext::DrawIndexed()
209: Sphere
212: ID3D11DeviceContext::DrawIndexed()
222: Sphere
225: ID3D11DeviceContext::DrawIndexed()
235: Sphere
238: ID3D11DeviceContext::DrawIndexed()
248: Sphere
251: ID3D11DeviceContext::DrawIndexed()
261: Sphere
264: ID3D11DeviceContext::DrawIndexed()
274: Sphere
277: ID3D11DeviceContext::DrawIndexed()
287: Sphere
290: ID3D11DeviceContext::DrawIndexed()
300: Sphere
303: ID3D11DeviceContext::DrawIndexed()
313: Sphere
316: ID3D11DeviceContext::DrawIndexed()
317: ID3DUserDefinedAnnotation::EndEvent()
319: ID3D11DeviceContext::ClearDepthStencilView()
321: Shadowmap
330: Floor
333: ID3D11DeviceContext::DrawIndexed()
334: empty label
335: ID3DUserDefinedAnnotation::EndEvent()
342: Base
345: ID3D11DeviceContext::DrawIndexed()
352: Center sphere
355: ID3D11DeviceContext::DrawIndexed()
362: Cone
365: ID3D11DeviceContext::DrawIndexed()
372: Cone
375: ID3D11DeviceContext::DrawIndexed()
382: Cone
385: ID3D11DeviceContext::DrawIndexed()
392: Cone
395: ID3D11DeviceContext::DrawIndexed()
402: Cone
405: ID3D11DeviceContext::DrawIndexed()
412: Cone
415: ID3D11DeviceContext::DrawIndexed()
422: Cone
425: ID3D11DeviceContext::DrawIndexed()
432: Cone
435: ID3D11DeviceContext::DrawIndexed()
442: Cone
445: ID3D11DeviceContext::DrawIndexed()
452: Cone
455: ID3D11DeviceContext::DrawIndexed()
462: Sphere
465: ID3D11DeviceContext::DrawIndexed()
472: Sphere
475: ID3D11DeviceContext::DrawIndexed()
482: Sphere
485: ID3D11DeviceContext::DrawIndexed()
492: Sphere
495: ID3D11DeviceContext::DrawIndexed()
502: Sphere
505: ID3D11DeviceContext::DrawIndexed()
512: Sphere
515: ID3D11DeviceContext::DrawIndexed()
522: Sphere
525: ID3D11DeviceContext::DrawIndexed()
532: Sphere
535: ID3D11DeviceContext::DrawIndexed()
542: Sphere
545: ID3D11DeviceContext::DrawIndexed()
552: Sphere
555: ID3D11DeviceContext::DrawIndexed()
556: ID3DUserDefinedAnnotation::EndEvent()
558: ID3D11DeviceContext::ClearRenderTargetView()
559: ID3D11DeviceContext::ClearDepthStencilView()
561: Lighting
563: ID3D11DeviceContext::ClearRenderTargetView()
564: ID3D11DeviceContext::ClearRenderTargetView()
566: Point light
580: ID3D11DeviceContext::DrawIndexed()
581: Sun light
597: ID3D11DeviceContext::DrawIndexed()
600: Cube light
614: ID3D11DeviceContext::DrawIndexed()
615: ID3DUserDefinedAnnotation::EndEvent()
617: ID3D11DeviceContext::ClearRenderTargetView()
618: ID3D11DeviceContext::ClearDepthStencilView()
620: Shading
630: ID3D11DeviceContext::Draw()
645: ID3D11DeviceContext::DrawIndexed()
652: ID3DUserDefinedAnnotation::EndEvent()
653: ID3DUserDefinedAnnotation::EndEvent()
654: Present(ResourceId::47)
Pass #0 starts with 2: ID3D11DeviceContext::ClearRenderTargetView()
Pass #0 contained 23 actions
Pass #1 starts with 319: ID3D11DeviceContext::ClearDepthStencilView()
Pass #1 contained 23 actions
Pass #2 starts with 558: ID3D11DeviceContext::ClearRenderTargetView()
Pass #2 contained 3 actions
Pass #3 starts with 617: ID3D11DeviceContext::ClearRenderTargetView()
Pass #3 contained 3 actions
@@ -1,200 +0,0 @@
import renderdoc as rd
import threading
import time
# This sample is intended as an example of how to do remote capture and replay
# as well as using device protocols to automatically enumerate remote targets.
#
# It is not complete since it requires filling in with custom logic to select
# the executable and trigger the capture at the desired time
raise RuntimeError("This sample should not be run directly, read the source")
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
protocols = rd.GetSupportedDeviceProtocols()
print(f"Supported device protocols: {protocols}")
# Protocols are optional - they allow automatic detection and management of
# devices.
if protocol_to_use is not None:
# the protocol must be supported
if protocol_to_use not in protocols:
raise RuntimeError(f"{protocol_to_use} protocol not supported")
protocol = rd.GetDeviceProtocolController(protocol_to_use)
devices = protocol.GetDevices()
if len(devices) == 0:
raise RuntimeError(f"no {protocol_to_use} devices connected")
# Choose the first device
dev = devices[0]
name = protocol.GetFriendlyName(dev)
print(f"Running test on {dev} - named {name}")
URL = protocol.GetProtocolName() + "://" + dev
# Protocols can enumerate devices which are not supported. Capture/replay
# is not guaranteed to work on these devices
if not protocol.IsSupported(URL):
raise RuntimeError(f"{dev} doesn't support capture/replay - too old?")
# Protocol devices may be single-use and not support multiple captured programs
# If so, trying to execute a program for capture is an error
if not protocol.SupportsMultiplePrograms(URL):
# check to see if anything is running. Just use the URL
ident = rd.EnumerateRemoteTargets(URL, 0)
if ident != 0:
raise RuntimeError(f"{name} already has a program running on {ident}")
else:
# If you're not using a protocol then the URL can simply be a hostname.
# The remote server must be running already - how that is done is up
# to you. Everything else will work the same over a normal TCP connection
protocol = None
URL = hostname
# Let's try to connect
result,remote = rd.CreateRemoteServerConnection(URL)
if result == rd.ResultCode.NetworkIOFailed and protocol is not None:
# If there's just no I/O, most likely the server is not running. If we have
# a protocol, we can try to start the remote server
print("Couldn't connect to remote server, trying to start it")
result = protocol.StartRemoteServer(URL)
if result != rd.ResultCode.Succeeded:
raise RuntimeError(f"Couldn't launch remote server, got error {str(result)}")
# Try to connect again!
result,remote = rd.CreateRemoteServerConnection(URL)
if result != rd.ResultCode.Succeeded:
raise RuntimeError(f"Couldn't connect to remote server, got error {str(result)}")
# We now have a remote connection. This works regardless of whether it's a device
# with a protocol or not. In fact we are done with the protocol at this point
protocol = None
print("Got connection to remote server")
# GetHomeFolder() gives you a good default path to start with.
# ListFolder() lists the contents of a folder and can recursively
# browse the remote filesystem.
home = remote.GetHomeFolder()
paths = remote.ListFolder(home)
print(f"Executables in home folder '{home}':")
for p in paths:
print(" - " + p.filename)
# Select your executable, perhaps hardcoded or browsing using the above
# functions
exe,workingDir,cmdLine,env,opts = select_executable()
print(f"Running {exe}")
result = remote.ExecuteAndInject(exe, workingDir, cmdLine, env, opts)
if result.result != rd.ResultCode.Succeeded:
remote.ShutdownServerAndConnection()
raise RuntimeError(f"Couldn't launch {exe}, got error {str(result.result)}")
# Spin up a thread to keep the remote server connection alive while we make a capture,
# as it will time out after 5 seconds of inactivity
def ping_remote(remote, kill):
success = True
while success and not kill.is_set():
success = remote.Ping()
time.sleep(1)
kill = threading.Event()
ping_thread = threading.Thread(target=ping_remote, args=(remote,kill))
ping_thread.start()
# Create target control connection
target = rd.CreateTargetControl(URL, result.ident, 'remote_capture.py', True)
if target is None:
kill.set()
ping_thread.join()
remote.ShutdownServerAndConnection()
raise RuntimeError(f"Couldn't connect to target control for {exe}")
print("Connected - waiting for desired capture")
# Wait for the capture condition we want
capture_condition()
print("Triggering capture")
target.TriggerCapture(1)
# Pump messages, keep waiting until we get a capture message. Time out after 30 seconds
msg = None
start = time.clock()
while msg is None or msg.type != rd.TargetControlMessageType.NewCapture:
msg = target.ReceiveMessage(None)
if time.clock() - start > 30:
break
# Close the target connection, we're done either way
target.Shutdown()
target = None
# Stop the background ping thread
kill.set()
ping_thread.join()
# If we didn't get a capture, error now
if msg.type != rd.TargetControlMessageType.NewCapture:
remote.ShutdownServerAndConnection()
raise RuntimeError("Didn't get new capture notification after triggering capture")
cap_path = msg.newCapture.path
cap_id = msg.newCapture.captureId
print(f"Got new capture at {cap_path} which is frame {msg.newCapture.frameNumber} with {msg.newCapture.api}")
# We could save the capture locally
# remote.CopyCaptureFromRemote(cap_path, local_path, None)
# Open a replay. It's recommended to set no proxy preference, but you could
# call remote.LocalProxies and choose an index.
#
# The path must be remote - if the capture isn't freshly created then you need
# to copy it with remote.CopyCaptureToRemote()
result,controller = remote.OpenCapture(rd.RemoteServer.NoPreference, cap_path, rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
remote.ShutdownServerAndConnection()
raise RuntimeError(f"Couldn't open {cap_path}, got error {str(result)}")
# We can now use replay as normal.
#
# The replay is tunnelled over the remote connection, so you don't have to keep
# pinging the remote connection while using the controller. Use of the remote
# connection and controller can be interleaved though you should only access
# them from one thread at once. If they are both unused for 5 seconds though,
# the timeout will happen, so if the controller is idle it's advisable to ping
# the remote connection
sampleCode(controller)
print("Shutting down")
controller.Shutdown()
# We can still use remote here - e.g. capture again, replay something else,
# save the capture, etc
remote.ShutdownServerAndConnection()
rd.ShutdownReplay()
@@ -1,118 +0,0 @@
Remote Capture and Replay
=========================
This example is a bit different since it's not ready-to-run. It provides a template for how you can capture and replay on a remote machine, instead of the local machine. It also shows how to use device protocols to automatically manage devices.
First we can enumerate which device protocols are currently supported.
.. highlight:: python
.. code:: python
protocols = rd.GetSupportedDeviceProtocols()
Each string in the list corresponds to a protocol that can be used for managing devices. If we're using one we can call :py:func:`~renderdoc.GetDeviceProtocolController` passing the protocol name and retrieve the controller.
The controller provides a few methods for managing devices. First we can call :py:meth:`~renderdoc.DeviceProtocolController.GetDevices` to return a list of device IDs. The format of these device IDs is protocol-dependent but will be equivalent to a normal hostname. Devices may have human-readable names obtainable via :py:meth:`~renderdoc.DeviceProtocolController.GetFriendlyName`.
.. highlight:: python
.. code:: python
protocol = rd.GetDeviceProtocolController(protocol_to_use)
devices = protocol.GetDevices()
if len(devices) == 0:
raise RuntimeError(f"no {protocol_to_use} devices connected")
# Choose the first device
dev = devices[0]
name = protocol.GetFriendlyName(dev)
print(f"Running test on {dev} - named {name}")
URL = protocol.GetProtocolName() + "://" + dev
The URL will be used the same as we would use a hostname, when connecting for target control or remote servers.
Note that protocols may have additional restrictions - be sure to check :py:meth:`~renderdoc.DeviceProtocolController.IsSupported` to check if the device is expected to function at all, and :py:meth:`~renderdoc.DeviceProtocolController.SupportsMultiplePrograms` to see if it supports launching multiple programs. If multiple programs are not supported, you should ensure all running capturable programs are closed before launching a new one.
To begin with we create a remote server connection using :py:func:`~renderdoc.CreateRemoteServerConnection`. The URL is as constructed above for protocol-based connections, or a simple hostname/IP if we're connecting directly to remote machine.
If the connection fails, normally we must fail but if we have a device protocol available we can attempt to launch the remote server automatically using :py:meth:`~renderdoc.DeviceProtocolController.StartRemoteServer`.
.. highlight:: python
.. code:: python
if result == rd.ResultCode.NetworkIOFailed and protocol is not None:
# If there's just no I/O, most likely the server is not running. If we have
# a protocol, we can try to start the remote server
print("Couldn't connect to remote server, trying to start it")
result = protocol.StartRemoteServer(URL)
if result != rd.ResultCode.Succeeded:
raise RuntimeError(f"Couldn't launch remote server, got error {str(result)}")
# Try to connect again!
result,remote = rd.CreateRemoteServerConnection(URL)
.. note::
The remote server connection has a default timeout of 5 seconds. If the connection is unused for 5 seconds, the other side will disconnect and subsequent use of the interface will fail.
Once we have a remote server connection, we can browse the remote filesystem for the executable we want to launch using :py:meth:`~renderdoc.RemoteServer.GetHomeFolder` and :py:meth:`~renderdoc.RemoteServer.ListFolder`.
Then once we've selected the executable, we can launch the remote program for capturing with :py:meth:`~renderdoc.RemoteServer.ExecuteAndInject`. This function is almost identical to the local :py:func:`~renderdoc.ExecuteAndInject` except that it is not possible to wait for the program to exit.
In our sample, we now place the remote server connection on a background thread that will ping it each second to keep the connection alive while we use a target control connection to trigger a capture in the application.
.. highlight:: python
.. code:: python
def ping_remote(remote, kill):
success = True
while success and not kill.is_set():
success = remote.Ping()
time.sleep(1)
kill = threading.Event()
ping_thread = threading.Thread(target=ping_remote, args=(remote,kill))
ping_thread.start()
To connect to and control an application we use :py:func:`~renderdoc.CreateTargetControl` with the URL as before and the ident returned from :py:meth:`~renderdoc.RemoteServer.ExecuteAndInject`.
.. highlight:: python
.. code:: python
target = rd.CreateTargetControl(URL, result.ident, 'remote_capture.py', True)
# Here we wait for whichever condition you want
target.TriggerCapture(1)
There are a couple of ways to trigger a capture, both :py:meth:`~renderdoc.TargetControl.TriggerCapture` and :py:meth:`~renderdoc.TargetControl.QueueCapture` depending on whether you want a time-based or frame-based trigger. The application itself can also use the in-application API to trigger a capture.
The target control connection can be intermittently polled for messages using :py:meth:`~renderdoc.TargetControl.ReceiveMessage`, which keeps the connection alive and will return any new information such as the data for a new capture that has been created. A message of type :py:data:`~renderdoc.TargetControlMessageType.NewCapture` indicates a new capture has been created, and :py:data:`~renderdoc.TargetControlMessage.newCapture` contains the information including the path.
.. highlight:: python
.. code:: python
msg = target.ReceiveMessage(None)
# Once msg.type == rd.TargetControlMessageType.NewCapture has been retrieved
cap_path = msg.newCapture.path
cap_id = msg.newCapture.captureId
Once the capture has been found we are finished with the target control connection so we can shut it down and stop the background thread that was keeping the remote server connection alive. Using the remote server connection we can copy the capture back to the local machine with :py:meth:`~renderdoc.RemoteServer.CopyCaptureFromRemote`. Similarly if we wanted to load a previously made capture that wasn't on the remote machine :py:meth:`~renderdoc.RemoteServer.CopyCaptureToRemote` would be useful to copy it ready to be opened.
Finally to open the capture we use, and that returns a :py:class:`~renderdoc.ReplayController` which can be used as normal and will tunnel over the remote server connection. It can be useful to intermittently ping the remote server connection to check that it's still valid, and remote server and controller calls can be interleaved as long as they don't overlap on multiple threads.
Example Source
--------------
.. only:: html and not htmlhelp
:download:`Download the example script <remote_capture.py>`.
.. literalinclude:: remote_capture.py
@@ -1,116 +0,0 @@
import sys
# Import renderdoc if not already imported (e.g. in the UI)
if 'renderdoc' not in sys.modules and '_renderdoc' not in sys.modules:
import renderdoc
# Alias renderdoc for legibility
rd = renderdoc
# 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.GetRootActions():
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.GetName(controller.GetStructuredFile())))
# 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
result = cap.OpenFile(filename, '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
return (cap, controller)
if 'pyrenderdoc' in globals():
pyrenderdoc.Replay().BlockInvoke(sampleCode)
else:
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
if len(sys.argv) <= 1:
print('Usage: python3 {} filename.rdc'.format(sys.argv[0]))
sys.exit(0)
cap,controller = loadCapture(sys.argv[1])
sampleCode(controller)
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
@@ -1,25 +0,0 @@
Save a texture to disk
======================
In this example we will find a particular action, and save the color output to disk as an image file.
To begin with, so that we have an interesting action selected we iterate over the list of actions finding the draw with the highest vertex count. For more on how to iterate through a capture's list of actions, see :doc:`iter_actions`.
Once we have set the draw 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
@@ -1,32 +0,0 @@
import renderdoc as rd
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
result = cap.OpenFile('test.rdc', '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
# Now we can use the controller!
print("%d top-level actions" % len(controller.GetRootActions()))
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
@@ -1,133 +0,0 @@
Getting Started (python)
========================
.. note::
This document is aimed at users getting started with loading a capture and getting access from the renderdoc module, and is generally not relevant when running within the RenderDoc UI.
The same APIs are available in the UI, so you can follow these steps. Be aware that loading captures while purely from script may interfere with a capture that is loaded in the UI itself, so this is not recommended.
Loading the Module
------------------
For this section we assume you have built a copy of RenderDoc and have the module (``renderdoc.pyd`` or ``renderdoc.so`` depending on your platform). For information on how to build see the `GitHub repository <https://github.com/baldurk/renderdoc>`_.
.. note::
You must use exactly the same version of python to load the module as was used to build it.
On windows by default RenderDoc builds against python 3.6 which is what it's distributed with.
This can be overridden by setting an overridden path under the ``Python Configuration`` section in the properties of the ``qrenderdoc`` project and ``pyrenderdoc_module``/``qrenderdoc_module`` projects. It must point to a python installation.
RenderDoc requires ``pythonXY.lib``, include files such as include/Python.h, as well as a .zip of the standard library. If you installed python with an installer you have the first two, and can generate the standard library zip by zipping the contents of the Lib folder. If you downloaded the embeddable zip distribution you will only have the standard library zip, you need to obtain the include files and ``.lib`` file separately.
Once you have the module, either place the module within your python's default library search path, or else insert the location of the python module into the path in your script. You can either set the ``PYTHONPATH`` environment variable or do it at the start of your script:
.. highlight:: python
.. code:: python
import sys
sys.path.append('/path/to/renderdoc/module')
Additionally, the renderdoc python module needs to be able to load the main renderdoc library (``renderdoc.dll`` or ``librenderdoc.so`` depending on your platform) - the module library itself just contains stubs and python wrappers for the C++ interfaces. You can either place the renderdoc library in the system library paths, or solve it in a platform specific way. For example on windows you can either place ``renderdoc.dll`` in the same directory as the python module, or append to ``PATH``. On Python 3.8 and above ``PATH`` is no longer searched by default so you need to explicitly add the DLL folder:
.. highlight:: python
.. code:: python
import os, sys
os.environ["PATH"] += os.pathsep + os.path.abspath('/path/to/renderdoc/native/library')
if sys.platform == 'win32' and sys.version_info[1] >= 8:
os.add_dll_directory("/path/to/renderdoc/native/library")
On linux you'd perform a similar modification to ``LD_LIBRARY_PATH``.
Assuming all has gone well, you should now be able to import the renderdoc module:
.. highlight:: python
.. code:: python
import renderdoc as rd
# Prints 'CullMode.FrontAndBack'
print(rd.CullMode.FrontAndBack)
Loading a Capture
-----------------
Given a capture file ``test.rdc`` we want to load it, begin the replay and get ready to perform analysis on it.
Before doing anything, we must initialise the replay API. We do that by calling :py:meth:`~renderdoc.InitialiseReplay`. Generally no special configuration is needed so passing a default :py:class:`~renderdoc.GlobalEnvironment` and an empty list of arguments is fine.
.. highlight:: python
.. code:: python
rd.InitialiseReplay(rd.GlobalEnvironment(), [])
To begin with, we use :py:meth:`~renderdoc.OpenCaptureFile` to obtain a :py:class:`~renderdoc.CaptureFile` instance. This gives us access to control over a capture file at a meta level. For more information see the :py:class:`renderdoc.CaptureFile` reference - the interface can also be used to create.
To open a file, use :py:meth:`~renderdoc.CaptureFile.OpenFile` on the :py:class:`~renderdoc.CaptureFile` instance. This function allows conversion from other formats via an importer, but here we'll use it just for opening a regular ``rdc`` file. It returns a :py:class:`~renderdoc.ResultDetails` which can be used to determine what went wrong in the event that there was a problem. We then check that the capture uses an API which can be replayed locally - for example not every platform supports ``D3D11``, so on linux this would return no local replay support.
.. highlight:: python
.. code:: python
# Open a capture file handle
cap = rd.OpenCaptureFile()
# Open a particular file - see also OpenBuffer to load from memory
result = cap.OpenFile('test.rdc', '', None)
# Make sure the file opened successfully
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't open file: " + str(result))
# Make sure we can replay
if not cap.LocalReplaySupport():
raise RuntimeError("Capture cannot be replayed")
Accessing Capture Analysis
--------------------------
Once the capture has been loaded, we can now begin the replay analysis. To do that we use :py:meth:`~renderdoc.CaptureFile.OpenCapture` which returns a tuple of :py:class:`~renderdoc.ResultDetails` and :py:class:`~renderdoc.ReplayController`.
This function call will open the capture and begin to replay it, and initialise the analysis. The :py:class:`~renderdoc.ReplayController` returned is the interface to the majority of RenderDoc's replaying functionality.
.. highlight:: python
.. code:: python
# Initialise the replay
result,controller = cap.OpenCapture(rd.ReplayOptions(), None)
if result != rd.ResultCode.Succeeded:
raise RuntimeError("Couldn't initialise replay: " + str(result))
# Now we can use the controller!
print("%d top-level actions" % len(controller.GetRootActions()))
Once we're done with the interfaces, we should call the ``Shutdown`` function on each, this allows the C++ interface to release the resources allocated.
Once all work is done we can shutdown the replay API.
.. highlight:: python
.. code:: python
# Shutdown the controller first, then the capture file
controller.Shutdown()
cap.Shutdown()
rd.ShutdownReplay()
Example Source
--------------
The full source for this example is available below:
.. only:: html and not htmlhelp
:download:`Download the example script <renderdoc_intro.py>`.
.. literalinclude:: renderdoc_intro.py