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