Add writeups of each example

This commit is contained in:
baldurk
2026-08-13 21:05:11 +01:00
parent ec5cd1f049
commit ed38da72ab
17 changed files with 1741 additions and 2 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+114
View File
@@ -0,0 +1,114 @@
Example: Adding a custom event browser filter
=============================================
The event browser allows a :doc:`filter expression <../../how/how_filter_events>` which can determine which events are shown and which aren't, defaulting to showing all :ref:`action events <actions>`.
The builtin filters allow filtering by simple strings but also by things like function parameters, parents/children for markers, or by regular expressions. Each special filter can have parameters to e.g. show all draws with more than 1000 indices with ``$action(numIndices > 1000)``.
It is possible to write a custom filter in python, allowing you to implement arbitrarily complex logic. This would best be done as a :doc:`UI extension <../ui_extensions>` so the filter is registered persistently, but we will demonstrate this with a simple script.
Registration
------------
To begin with, we need to register our filter. We provide the function name, which for us is ``prime`` meaning the filter will be ``$prime(...)``, as well as a description text that will be shown in the help window for users.
In this example, we unconditionally unregister the filter first to ensure that the registration succeeds. Normally in a UI extension you would register and unregister in the corresponding extension functions.
.. highlight:: python
.. code:: python
pyrenderdoc.GetEventBrowser().UnregisterEventFilterFunction("prime")
pyrenderdoc.GetEventBrowser().RegisterEventFilterFunction(
"prime",
"Show only events with prime EIDs.",
filter_func,
parser_func,
completer_func,
)
We provide three callback functions, one for doing the actual filtering, one for parsing parameters, and one for auto-complete. Only the filtering function is required, so if you don't need any parameters you can pass ``None`` for the parser and completer functions.
Filtering
---------
The filter function is called once for each candidate event and returns a ``bool`` indicating whether this event should be included or excluded. The function is passed a few parameters directly for the event which are useful. You are given the :class:`~qrenderdoc.CaptureContext` again for any queries needed, as well as the name of your filter (in case you have a multi-dispatch function) and the parameters passed.
Per event you are also given the :ref:`event ID <eventids>`, the :class:`~renderdoc.SDChunk` for looking up :ref:`API parameters <apiparams>`, the :class:`~renderdoc.ActionDescription` for actions (or ``None`` if the event is not an action), and a string name that is shown in the event browser.
Our extremely useful filter function just checks to see if the EID is prime or not.
.. highlight:: python
.. code:: python
# prime numbers have exactly 2 integer factors.
# note the // operator in python does integer-division
def isprime(n):
return len([x for x in range(1, n + 1) if (n / x) == (n // x)]) == 2
def filter_func(
ctx: qrenderdoc.CaptureContext,
filter: str,
params: str,
eventId: int,
chunk: renderdoc.SDChunk,
action: renderdoc.ActionDescription,
eventName: str,
):
return isprime(eventId)
.. figure:: ../../imgs/python/FilterExample.png
Our filter showing the prime events
Parsing
-------
Optionally filter functions can take parameters. There is no strict rule to what the parameter string must be, but users will likely expect it to look like an expression. You are responsible for parsing the string yourself, which you can do in this function.
This function is called once whenever the filter changes, and so can also be a good time to cache more expensive data rather than repeatedly re-calculating something in the filter function.
The function should return a string with any errors it wants to report - or an empty string if there are none. If errors are reported the filter won't be evaluated and the errors will be highlighted to the user.
In our case we simply look for the string 'error' and disallow that.
.. highlight:: python
.. code:: python
def parser_func(ctx: qrenderdoc.CaptureContext, filter: str, params: str):
if "error" in params:
return f"You shouldn't put 'error' in ${filter}()"
return ""
.. figure:: ../../imgs/python/FilterError.png
The parser showing an error for our filter
Auto-complete
-------------
Going hand-in-hand with parsing arguments, it is also optionally possible to provide auto-complete suggestions for users. These suggestions are not fixed and can be done contextually based on the current (partial) parameters string.
Even if you have a parsing function, the auto-completion function is optional so you can omit it.
In our example we just return a fixed set of strings, including an error string which shows off our parsing.
.. highlight:: python
.. code:: python
def completer_func(ctx: qrenderdoc.CaptureContext, filter: str, params: str):
return ["foo", "bar", "error"]
.. figure:: ../../imgs/python/FilterComplete.png
The auto-complete options for our filter
Example Source
--------------
This example can be found under the name "Custom event filter" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <event_filter.py>`.
.. literalinclude:: event_filter.py
@@ -0,0 +1,90 @@
Example: Launching an application
=================================
In this example we will show how you can utilise the UI's :doc:`../../window/capture_attach` to configure and launch an application for capture, which could be useful as a way of automating workflows or tests.
Configuring capture
-------------------
After showing and getting the capture dialog handle, we can directly set things like the executable path, command line, or also working directory as needed. In our case we ask the user to browse to the executable. We set a filter of ``*.exe`` which is only relevant on windows, you could change this as needed. These common properties can be set directly with helper functions
.. highlight:: python
.. code:: python
pyrenderdoc.ShowCaptureDialog()
dialog = pyrenderdoc.GetCaptureDialog()
exe = pyrenderdoc.Extensions().OpenFileName("Find an executable", "", "*.exe")
dialog.SetExecutableFilename(exe)
dialog.SetCommandLine("--cool-level very")
For setting more specific functionality, we can grab the whole set of settings. This contains everything about the configuration for launching an application including any capture options.
.. highlight:: python
.. code:: python
settings = dialog.Settings()
# we could also set the command line here, this is identical to SetCommandLine() above
print(settings.commandLine)
# reset anything the user has changed to default
settings.options = renderdoc.CaptureOptions()
# enable callstack capture
settings.options.captureCallstacks = True
dialog.SetSettings(settings)
Launching capture
-----------------
Once we have set the configuration, we double-check with the user - this is optional but good for our example. If they click yes, we will launch the capture.
.. highlight:: python
.. code:: python
opts = [qrenderdoc.DialogButton.Yes, qrenderdoc.DialogButton.No]
go = pyrenderdoc.Extensions().QuestionDialog("Ready to Launch?", opts, "Final Check")
if go == qrenderdoc.DialogButton.Yes:
conn = dialog.Launch()
Capture connection
------------------
If the program has successfully launched, we will be returned a handle to the capture connection window that is added to the UI. This contains the details of the active connection and any captures made.
.. warning::
Capture connection windows are temporary and may close themselves if the program exits after not making any captures - at this point the connection handle we hold will no longer be valid. You should either register a callback to be invoked when the connection closes itself with :meth:`~qrenderdoc.CaptureConnection.RegisterClosedCallback`, or prevent this auto-closing with :meth:`~qrenderdoc.CaptureConnection.PreventAutoClose`.
With this connection it is possible to control the capture and opening of captures, which is outside the scope of this example. We will take a very simple extra step, having a callback after 5 seconds which prints the connected program and the active graphics APIs that have been initialised.
.. highlight:: python
.. code:: python
def connected_cb():
print(f"Connected to {conn.Target()} running APIs: {', '.join(conn.GetAPIs())}")
numcaps = len(conn.GetCaptures())
if numcaps == 0:
print("No captures have been made!")
else:
print(f"{numcaps} captures have been made!")
# wait a little bit, then call our callback to print the connection status
pyrenderdoc.DelayedCallback(5000, connected_cb)
Example Source
--------------
This example can be found under the name "Launching an application" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <exe_launching.py>`.
.. literalinclude:: exe_launching.py
+345
View File
@@ -0,0 +1,345 @@
Example: Pixel History & Shader Debug
=====================================
Two of the more complex and closely related analysis steps you can perform in RenderDoc is fetching the history of modifications to a given pixel in a texture, and debugging shaders to get a trace of what happened with its execution.
In this sample we will run a given pixel history, and then debug one of the pixel shaders that executed.
Choosing a pixel
----------------
To begin with we need to find a pixel, and the easiest way to do this is to let the user choose it naturally in the texture viewer. In addition to our :doc:`typical start <index>` that loads a capture we will also afterwards prompt the user to pick a pixel.
We do this by calling a function that will query the user if they are ready - if not the function will call itself back in 5 seconds via delayed callback to ask again.
Once the user has gone to the texture viewer and picked a pixel they like at the right event, they can click yes and we will proceed to do the actual work.
.. highlight:: python
.. code:: python
def check_ready():
choice = pyrenderdoc.Extensions().QuestionDialog(
"Are you at an interesting event with pixel selected?",
[qrenderdoc.DialogButton.Yes, qrenderdoc.DialogButton.No, qrenderdoc.DialogButton.Cancel],
"Ready?",
)
if choice == qrenderdoc.DialogButton.Cancel:
return
if choice == qrenderdoc.DialogButton.No:
pyrenderdoc.DelayedCallback(5000, check_ready)
return
prepare_history()
check_ready()
Configuring Pixel History
-------------------------
From the :class:`~qrenderdoc.TextureViewer` class that we can obtain (:meth:`~qrenderdoc.CaptureContext.GetTextureViewer`), we can query for the selected texture (:meth:`~qrenderdoc.TextureViewer.GetCurrentResource`), subresource (:meth:`~qrenderdoc.TextureViewer.GetSelectedSubresource`), and pixel (:meth:`~qrenderdoc.TextureViewer.GetPickedLocation`).
.. highlight:: python
.. code:: python
tex_view = pyrenderdoc.GetTextureViewer()
# find the selected texture and location
id = tex_view.GetCurrentResource()
sub = tex_view.GetSelectedSubresource()
x, y = tex_view.GetPickedLocation()
Once we've done this we can open a new pixel history viewer (:meth:`~qrenderdoc.CaptureContext.ViewPixelHistory`) in preparation. Because pixel history can be a long-running process it is a good user experience to load the viewer first and display it, then later fill it with data once obtained.
We will open it using the properties that we have queried, and then display it (:meth:`~qrenderdoc.CaptureContext.AddDockWindow`) on the right hand side of the python shell wherever it is currently docked.
.. highlight:: python
.. code:: python
history_window = pyrenderdoc.ViewPixelHistory(id, x, y, sub.slice, disp)
pyrenderdoc.AddDockWindow(
history_window.Widget(),
qrenderdoc.DockReference.RightOf,
pyrenderdoc.GetPythonShell().Widget(),
)
Running Pixel History
---------------------
As we mentioned above, pixel history can be a fairly long running task that takes many seconds to complete depending on the complexity of the capture. This is a good time to consider :ref:`pythreading` and how to arrange the work such that the UI does not become unresponsive.
If writing a UI extension, by default code will be running on the UI thread so you are recommended to use :meth:`~qrenderdoc.ReplayManager.AsyncInvoke` and :meth:`~qrenderdoc.CaptureContext.InvokeOntoUIThread`. First you call a callback on the replay thread to do the long-running analysis work, then call a callback on the UI thread for any updating of views or displaying information to the user.
Since we are running this in the python scripting panel, we can take advantage of that script running in a separate thread itself and instead use the convenience helper :meth:`~qrenderdoc.CaptureContext.GetBlockingController` to obtain a blocking version of the :class:`~renderdoc.ReplayController`. This will cause the python thread to stall while the pixel history works but the UI will remain responsive. Once the data is returned, we can pass it to the UI display.
.. highlight:: python
.. code:: python
controller = pyrenderdoc.GetBlockingController()
history = controller.PixelHistory(id, x, y, sub, disp.typeCast)
history_window.SetHistory(history)
Examining Pixel History
-----------------------
The pixel history is returned as a list of :class:`~renderdoc.PixelModification` structures. Each of these structures corresponds to one instance of the pixel potentially being modified. We can examine this ourselves to process the data programmatically in addition to the UI panel we opened above.
.. note::
It is possible to have multiple modifications in one event! This can happen if there is a draw with multiple polygons overlapping the same pixel.
You can use :data:`~renderdoc.PixelModification.fragIndex` together with :data:`~renderdoc.PixelModification.eventId` to identify this case.
The modification structure itself contains as much information about the modification as possible, but depending on the API details or the particular event not all information may be available.
Each modification contains the value before modification (:data:`~renderdoc.PixelModification.preMod`), the value after modification (:data:`~renderdoc.PixelModification.postMod`), and the value output from the shader (:data:`~renderdoc.PixelModification.shaderOut`) which are all of type :class:`~renderdoc.ModificationValue`.
Not all of this information is always available, for example on events without shaders like clears. In other cases it may not be possible to obtain all information such as in secondary command buffers on vulkan. If a value is not available :meth:`~renderdoc.ModificationValue.IsValid` will return false.
Pixel history can give the most information for rasterized modifications but pixels in a texture can also be modified via direct writes from shaders. In this case :data:`~renderdoc.PixelModification.directShaderWrite` will be true indicating that the texture was bound for write at an event. When true, most other information apart from pre- and post- modification values will be unavailable as RenderDoc does not instrument shader writes to determine whether the texel was modified and if so by what.
A useful high level helper is :meth:`~renderdoc.PixelModification.Passed` which returns true if the pixel did not fail any known or detectable test. This does not *guarantee* that the pixel was modified especially in the case of direct shader writes. If a pixel did not pass, then there are bool properties in :class:`~renderdoc.PixelModification` for different fixed function tests or checks it may have failed.
Launching pixel debug
---------------------
Once we have our list of history events, we can try to debug the pixel shader in one of them. To do that we filter the list of events to those that have:
#. Passed all known tests
#. Have a pixel shader bound
#. Are an event that is a draw call
Picking the first of these, we can then :ref:`move to that event <currentevent>` and try to initiate a pixel debug (:meth:`~renderdoc.ReplayController.DebugPixel`). First we need to check the shader reflection data to make sure it is :data:`~renderdoc.ShaderDebugInfo.debuggable`. Not all shader constructs are currently supported, so we query and print an error if there is something that can't be debugged.
.. highlight:: python
.. code:: python
if not refl.debugInfo.debuggable:
print("Shader can't be debugged:")
print(refl.debugInfo.debugStatus)
return
Launching the debug is done via :meth:`~renderdoc.ReplayController.DebugPixel` which takes the obvious x, y but also a small configuration struct (:class:`~renderdoc.DebugPixelInputs`) with other properties to narrow the candidate pixel to debug. To handle the case of multiple overlapping polygons we use the :data:`~renderdoc.PixelModification.primitiveID` we hopefully got from pixel history. If we didn't get a primitive ID the debugger will pick an arbitrary instance of the pixel shader at that co-ordinate to debug.
The return value will be a debug trace to be owned by python, which is ready for further processing. If the trace failed to initialise for any reason, the :data:`~renderdoc.ShaderDebugTrace.debugger` member will be ``None``.
.. highlight:: python
.. code:: python
inputs = renderdoc.DebugPixelInputs()
inputs.primitive = p.primitiveID
inputs.sample = renderdoc.ReplayController.NoPreference
inputs.view = renderdoc.ReplayController.NoPreference
trace = controller.DebugPixel(x, y, inputs)
if trace.debugger is None:
print("Debug failed :(")
controller.FreeTrace(trace)
return
.. warning::
It is important to note that the trace is owned by the caller, and must be freed with :data:`~renderdoc.ReplayController.FreeTrace` once you are done with it, to avoid a leak. For more information see :doc:`../in_depth/lifetimes`.
Simulating debug session
------------------------
The trace itself contains global information which is not specific to any particular step of the execution of the shader. For example it will contain bound resources (:data:`~renderdoc.ShaderDebugTrace.readOnlyResources`), and constant data (:data:`~renderdoc.ShaderDebugTrace.constantBlocks`), as well as the input values (:data:`~renderdoc.ShaderDebugTrace.inputs`) which will differ by shader stage.
We will get into other members below, but the important one to consider first is the :data:`~renderdoc.ShaderDebugTrace.debugger` member. This is an opaque handle to the debug engine which is now set up to begin simulating execution of a shader.
RenderDoc's debug engine simulates in small steps to allow incremental processing and display as needed. For simplicity and in the common case we will just repeatedly simulate until it is complete. This is done by calling :meth:`~renderdoc.ReplayController.ContinueDebug` which returns a list of :class:`~renderdoc.ShaderDebugState`, until it is complete when it returns an empty list.
.. highlight:: python
.. code:: python
states: List[renderdoc.ShaderDebugState] = []
# continually simulate the shader until it completes
while True:
more = controller.ContinueDebug(trace.debugger)
if more == []:
break
states += more
Examining debug shader states
-----------------------------
The main trace of the shader's execution is now represented in this list of :class:`~renderdoc.ShaderDebugState`. Each state represents a single atomic execution - usually one instruction or indivisible set of instructions. At each step you can see the step index overall (which linearly increases from 0), the next instruction that will be executed, the callstack, and any changes that happened to debug variables.
The first state at :data:`~renderdoc.ShaderDebugState.stepIndex` 0 is the state immediately before any shader instructions have executed, this then linearly increases by one at each step so ``states[x].stepIndex == x`` in our case. The next state is the state immediately after the first instruction, and before the second.
Each state lists the :data:`~renderdoc.ShaderDebugState.nextInstruction` which will be executed. Each instruction has a unique index but these indices are not compactly numbered starting from 0 - different shader representations may vary in different APIs.
Each state also lists the :data:`~renderdoc.ShaderDebugState.changes` to debug variables. Debug variables are indexed by name and so may be arbitrary, but will generally follow normal identifier rules and should be grouped with ``foo.bar`` being considered to represent a member ``bar`` in a parent ``foo``, and similarly for ``foo[2]`` being index ``2`` in an array ``foo[]``.
Changes to these variables are given with the value they have before, and the value after. This means it is easy to step forwards or backwards across a state without needing to store significant amounts of data as the information is bi-directional. The first time a variable is seen, its :data:`~renderdoc.ShaderVariableChange.before` will be an uninitialised :class:`~renderdoc.ShaderVariable`, to indicate that it did not exist before. If a variable goes out of scope or otherwise exits its lifespan then instead its :data:`~renderdoc.ShaderVariableChange.after` will be an uninitialised :class:`~renderdoc.ShaderVariable`.
.. tip::
This representation is convenient for walking back and forward through the steps, but it also means there is no single lookup of all variables at any given step. If you need this you will need to walk the steps forward from the first step and accumulate variables as necessary.
.. highlight:: python
.. code:: python
# look at the mid-point state
if states == []:
print("Shader debug failed!")
else:
state = states[len(states) // 2]
print(
f"Examining step {state.stepIndex}, before instruction {state.nextInstruction}"
)
stack = "\n".join(state.callstack)
print(f"Callstack:\n{stack}")
print()
print(f"{len(state.changes)} debug variable changes")
for ch in state.changes:
print(f" '{ch.before.name}' -> '{ch.after.name}'")
print()
Source Variables and Instruction info
-------------------------------------
Debug variables as listed above are the variables directly simulated by the simulation such as registers or SSA values and are often hard to interpret. Most APIs provide optional debug information which can give information about how high-level source code and variables maps down to the variables and instructions being simulated.
This information is stored in the trace, in :data:`~renderdoc.ShaderDebugTrace.instInfo`. Each entry describes a single instruction as given by :data:`~renderdoc.InstructionSourceInfo.instruction`, which can be looked up by the instruction indices given in the debug steps. Not every instruction is guaranteed to have an entry, as for some cases the same debug info will apply to several adjacent instructions. If there is no direct match for an instruction, the closest previous match will apply.
.. warning::
As mentioned above, not all APIs use a compact list of instructions from ``0..n``. You should **not** index into the :data:`~renderdoc.ShaderDebugTrace.instInfo` array with the instruction index, but instead search it for the closest matching instruction number.
.. highlight:: python
.. code:: python
infos = [i for i in trace.instInfo if i.instruction <= state.nextInstruction]
if infos == []:
info = trace.instInfo[0]
else:
info = infos[-1]
Each instruction's information contains the line information for the instruction, giving both the line number in the default-generated disassembly (:meth:`~renderdoc.ReplayController.DisassembleShader`) as well as (if available) the line number and optionally column where the code mapped to in the original source (:data:`~renderdoc.ShaderDebugInfo.files`).
The information also gives information about high-level variables in the source that map to debug variables, possibly only partially available or mapped across different debug variables. At each instruction there is a list of :class:`~renderdoc.SourceVariableMapping` which provides the information about the type of the original variable as well as a component-wise list of debug variables (:class:`~renderdoc.DebugVariableReference`) that it maps to.
In our example we print out the disassembly and source line for the instruction, as well as a list of source variables.
.. highlight:: python
.. code:: python
disasm = controller.DisassembleShader(
pipe.GetGraphicsPipelineObject(),
refl,
"",
)
disline = disasm.splitlines()[info.lineInfo.disassemblyLine - 1]
srcline = ""
if info.lineInfo.fileIndex >= 0:
src = refl.debugInfo.files[info.lineInfo.fileIndex].contents
srcline = src.splitlines()[info.lineInfo.lineStart - 1]
print(f"Examining instruction {info.instruction}.")
print(f" which has {len(info.sourceVars)} source vars:")
if srcline != "":
print(srcline)
print(disline)
print()
for s in info.sourceVars:
debugVars = ", ".join(
[v.name + "." + ("xyzw"[v.component % 4]) for v in s.variables]
)
print(f" {s.name} is {str(s.type)} stored in: {debugVars}")
.. note::
If the shader being debugged does not have any debug information available, some or all of this may be unavailable.
Displaying debug session in the UI
----------------------------------
The results of a shader debug can be displayed in the UI, though not directly from the data we have obtained in python. Instead the UI shader viewer expects to be given ownership over the debug trace immediately after it is created so that it can perform the simulation steps with :meth:`~renderdoc.ReplayController.ContinueDebug` itself.
.. highlight:: python
.. code:: python
trace = controller.DebugPixel(x, y, inputs)
shad = pyrenderdoc.DebugShader(
pipe.GetShaderReflection(renderdoc.ShaderStage.Pixel),
pipe.GetGraphicsPipelineObject(),
trace,
"Debugged From Python",
)
pyrenderdoc.AddDockWindow(
shad.Widget(),
qrenderdoc.DockReference.BottomOf,
history_window.Widget(),
)
Sample Output
-------------
.. sourcecode:: text
Analysing on "Main Color Buffer"@{'mip': '0', 'sample': '0', 'slice': '0'} at 502,533
4 modifications to that pixel:
at EID 637 changed from [0.93, 0.67, 0.81, 1.0] to [0.0, 0.0, 0.0, 1.0]
at EID 812 modification failed
at EID 823 modification failed
at EID 831 changed from [0.0, 0.0, 0.0, 1.0] to [1.85, 0.49, 0.81, 1.0]
Examining step 653, before instruction 157
Callstack:
main
ApplyConeLight
ApplyLightCommon
1 debug variable changes
'r8' -> 'r8'
Examining instruction 157.
which has 24 source vars:
float3 halfVec = normalize(lightDir - viewDir);
157: dp3 r4.w, r8.xyzx, r8.xyzx
vsOutput.position is VarType.Float stored in: v0.x, v0.y, v0.z, v0.w
vsOutput.worldPos is VarType.Float stored in: v1.x, v1.y, v1.z
vsOutput.uv is VarType.Float stored in: v2.x, v2.y
vsOutput.viewDir is VarType.Float stored in: v3.x, v3.y, v3.z
vsOutput.shadowCoord is VarType.Float stored in: v4.x, v4.y, v4.z
vsOutput.normal is VarType.Float stored in: v5.x, v5.y, v5.z
vsOutput.tangent is VarType.Float stored in: v6.x, v6.y, v6.z
vsOutput.bitangent is VarType.Float stored in: v7.x, v7.y, v7.z
<main return value> is VarType.Float stored in: o0.x, o0.y, o0.z
diffuseAlbedo is VarType.Float stored in: r1.x, r1.y, r1.z
normal is VarType.Float stored in: r3.x, r3.y, r3.z
specularMask is VarType.Float stored in: r1.w
viewDir is VarType.Float stored in: r4.x, r4.y, r4.z
colorSum is VarType.Float stored in: r2.x, r2.y, r2.z
tileLightCountConeShadowed is VarType.UInt stored in: r6.y
tileLightCountCone is VarType.UInt stored in: r6.x
tileLightLoadOffset is VarType.UInt stored in: r0.y
lightData.radiusSq is VarType.Float stored in: r8.w
lightData.color is VarType.Float stored in: r9.x, r9.y, r9.z
lightData.coneDir is VarType.Float stored in: r10.x, r10.y, r10.z
lightData.coneAngles is VarType.Float stored in: r10.w
invLightDist is VarType.Float stored in: r4.w
lightDir is VarType.Float stored in: r11.x, r11.y, r11.z
distanceFalloff is VarType.Float stored in: r5.w
Example Source
--------------
This example can be found under the name "Pixel History & Shader Debug" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <history_debug.py>`.
.. literalinclude:: history_debug.py
+33
View File
@@ -0,0 +1,33 @@
Python Examples
===============
These examples show small snippets of different commonly used areas of RenderDoc and how to access them from the UI. The source for each example is available in the :guilabel:`Examples` section of the python scripting panel.
There is a common preamble in the source code used to help external IDEs know about the pre-provided module imports and global variable, which is explained in a :ref:`FAQ entry <example_preamble>`.
The examples will also usually check to see if a capture is open, and prompt for one if not. This is the same in each example and is not explained each time. This could be hardcoded, use some other logic to open a capture, or raise an exception if a capture isn't already open.
.. highlight:: python
.. code:: python
if not pyrenderdoc.IsCaptureLoaded():
filename = pyrenderdoc.Extensions().OpenFileName("Choose a capture", "", "*.rdc")
pyrenderdoc.LoadCapture(filename, renderdoc.ReplayOptions(), filename, False, True)
----------------
.. toctree::
:maxdepth: 1
show_buffer
show_texture
iter_actions
pipe_state
shader_refl
resource_usage
mem_binds
history_debug
mesh_output
exe_launching
event_filter
+141
View File
@@ -0,0 +1,141 @@
Example: Iterating over Actions
===============================
In this example we will walk through the actions in a capture and print them out as a quick tree summary.
Actions include broadly anything that can modify memory - this includes draws and dispatches, as well as clears/copies. Although not modifying memory, marker regions and marker labels are also considered actions and form a hierarchy of nested markers.
Beginning recursion of action tree
----------------------------------
Actions are stored in a tree structure :class:`~renderdoc.ActionDescription`. Each action may have 0 or more children stored in :data:`~renderdoc.ActionDescription.children` and so we will walk this tree with a recursive function.
Each action also stores links to the :data:`~renderdoc.ActionDescription.previousAction`, :data:`~renderdoc.ActionDescription.nextAction`, and :data:`~renderdoc.ActionDescription.parent` actions but you should note that these may be ``None``. Previous and next actions are based on the linear :ref:`event ID <eventids>` and can be used for linearly walking events.
.. tip::
The :class:`~qrenderdoc.EventBrowser` has some helpers for also fetching actions such as :meth:`~qrenderdoc.EventBrowser.GetActionForEID`.
The root of this recursion starts appropriately with the root actions obtained with :meth:`~qrenderdoc.CaptureContext.CurRootActions` - the list of actions in a capture which have no parents. We will call our recursive function with this list and it returns a list of strings to print.
.. highlight:: python
.. code:: python
for line in format_tree(pyrenderdoc.CurRootActions()):
print(line)
Recursing into marker regions
-----------------------------
Our function will receive a list of actions and process them. First we will define how we recurse, by checking :data:`~renderdoc.ActionDescription.flags`. These flags can be used to quickly check for the 'type' of action - and we look for :data:`~renderdoc.ActionFlags.PushMarker` or :data:`~renderdoc.ActionFlags.MultiAction`.
.. highlight:: python
.. code:: python
from typing import List
def format_tree(actions: List[renderdoc.ActionDescription]):
ret = []
for a in actions:
ActionFlags = renderdoc.ActionFlags
if a.flags & (ActionFlags.PushMarker | ActionFlags.MultiAction):
ret.append(f"{a.customName}:")
ret += [" " + l for l in format_tree(a.children)]
return ret
.. tip::
The import and use of ``typing.List`` is optional, python type annotations have no semantic meaning on the code, but they are useful to inform IDEs and RenderDoc's script editor of the type you expect for arguments and improve autocomplete. Without this, type checkers will not know the type of ``a`` in the loop and will not be able to provide autocomplete of its members.
This will look at each action, and whenever we encounter a marker region print the name of the marker region and then recursively call on the children with an indent. As we return a list of lines this makes it easy for us to have one indent level per level of recursion.
Counting other actions
----------------------
This will already form a complete recursion of the tree of markers and print them out, but we can also do more as we are walking through by counting the number of some other types of actions as we go.
These can also be identified via the :class:`~renderdoc.ActionFlags` flags.
.. highlight:: python
.. code:: python
def format_tree(actions: List[renderdoc.ActionDescription]):
draws, dispatches, copies = 0, 0, 0
ret = []
for a in actions:
ActionFlags = renderdoc.ActionFlags
if a.flags & (ActionFlags.PushMarker | ActionFlags.MultiAction):
...
# for non marker-regions, count them
elif a.flags & ActionFlags.Drawcall:
draws += 1
elif a.flags & ActionFlags.Dispatch:
dispatches += 1
elif a.flags & (ActionFlags.Copy | ActionFlags.Clear):
copies += 1
This allows us to check for some number of actions quickly via the flags and count them up individually.
Once we have the final counts and have finished iterating over the list of actions, we can format these counts into an extra line for returning.
.. highlight:: python
.. code:: python
# make a final line if we found anything else in this region
line = ""
if draws > 0:
line += f", {draws} draws"
if dispatches > 0:
line += f", {dispatches} dispatches"
if copies > 0:
line += f", {copies} clears/copies"
# trim the starting ", "
if line != "":
ret.insert(0, line[2:])
Final Output
------------
Depending on your capture, it may look something like this, with some markers having both children and draws/dispatches, and other markers only containing either a dispatch or other markers:
.. sourcecode:: text
Scene Render:
Particle Update:
7 dispatches, 4 clears/copies
ExecuteIndirect(maxCount 1, count <1>):
1 dispatches
ExecuteIndirect(maxCount 1, count <1>):
1 dispatches
ExecuteIndirect(maxCount 1, count <1>):
1 dispatches
RenderLightShadows:
34 draws, 2 clears/copies
Z PrePass:
Opaque:
29 draws, 1 clears/copies
Cutout:
5 draws
Generate SSAO:
Decompress and downsample:
2 dispatches
Analyze depth volumes:
5 dispatches
Blur and upsample:
3 dispatches
...
Example Source
--------------
This example can be found under the name "Iterating over Actions" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <iter_actions.py>`.
.. literalinclude:: iter_actions.py
+42
View File
@@ -0,0 +1,42 @@
Example: Memory bindings
========================
For Vulkan and D3D12 that support explicit memory binding of texture and buffer resources to memory, RenderDoc exposes this information to python. This example shows how to query that and some simple processing we can do with it
Memory information
------------------
In each of :class:`~renderdoc.TextureDescription` and :class:`~renderdoc.BufferDescription` there are two members - :data:`~renderdoc.TextureDescription.memory` and :data:`~renderdoc.TextureDescription.memoryOffset` which give the memory object being bound to, as well as the offset in that object.
For APIs where this binding does not happen explicitly, both members will be unset. This can also happen on Vulkan if the resource was created but never bound to memory, or on D3D12 if the resource was created as a committed resource with no separate memory object.
We will iterate over the list of textures (:meth:`~qrenderdoc.CaptureContext.GetTextures`) and buffers (:meth:`~qrenderdoc.CaptureContext.GetBuffers`) and store each memory range into a dictionary indexed by the memory object being bound to.
Finally we use a simple O(n\ :sup:`2`) check for any overlaps of resources, printing each one as we find it.
Sample Output
-------------
.. sourcecode:: text
In memory Memory 123 overlap:
05100000 - 06c00000: [Texture] PostProcessScratch1
05904000 - 05a84000: [Buffer] dynamic particles
In memory Memory 123 overlap:
02300000 - 055c8000: [Buffer] ScratchMemory1
02fe2000 - 05904000: [Buffer] ScratchMemory2
In memory Memory 123 overlap:
055c8000 - 08890000: [Buffer] ScratchMemory3
05904000 - 05a84000: [Buffer] DebugUIVertices
Example Source
--------------
This example can be found under the name "Memory bindings" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <mem_binds.py>`.
.. literalinclude:: mem_binds.py
+5 -2
View File
@@ -42,7 +42,10 @@ if meshdata.indexResourceId != renderdoc.ResourceId():
else:
print(" (non-indexed)")
if meshdata.unproject:
print(f" Rasterized data: {meshdata.nearPlane:.2f}-{meshdata.farPlane:.2f}")
far = f"{meshdata.farPlane:.2f}"
if meshdata.farPlane > 3.0e+38:
far = "inf"
print(f" Rasterized data: {meshdata.nearPlane:.2f}-{far}")
# default to just indices, but if this does use an index buffer then fetch that data
idxs = [i for i in range(meshdata.numIndices)]
@@ -57,7 +60,7 @@ if meshdata.indexResourceId != renderdoc.ResourceId():
# use struct.unpack to interpret the bytes as a series of integers
idxs = cast(
List[int], struct.unpack(f"{struct_type}{meshdata.numIndices}", bufdata)
List[int], struct.unpack_from(f"={meshdata.numIndices}{struct_type}", bufdata)
)
+276
View File
@@ -0,0 +1,276 @@
Example: Mesh Output
====================
RenderDoc is able to fetch and display the mesh output data at each shader stage, in the :doc:`../../window/mesh_viewer`. This data can also be queried and decoded in python, as we will show in this example.
It is worth noting that although this is written from the perspective of decoding the output data from mesh stages, a large amount of this applies equally to decoding general buffer data with a known format.
.. warning::
Decoding mesh output data *in the general case* to fully handle all possible API features, shader stages, and variable types can be very complex. This example will deliberately focus on a simple case where the reflection and types are easy to understand. From that basic foundation it is then possible to expand in different ways that are outside the scope of this example.
Selecting an action
-------------------
For this example we are keeping it simple, so we want to find an action that is a simple draw call that uses just a vertex shader. Task/mesh shaders may require different handling and the presence of tessellation or geometry shaders will also complicate matters.
Unlike other examples, we will fail to run if a capture is not loaded or a suitable action is not already selected.
.. highlight:: python
.. code:: python
pipe = pyrenderdoc.CurPipelineState()
avoid_stages = [
renderdoc.ShaderStage.Mesh,
renderdoc.ShaderStage.Geometry,
renderdoc.ShaderStage.Hull,
]
if any([pipe.GetShader(x) != renderdoc.ResourceId() for x in avoid_stages]):
raise RuntimeError("Can't run example on this draw")
refl = pipe.GetShaderReflection(renderdoc.ShaderStage.Vertex)
if refl is None:
raise RuntimeError("Can't run example on this draw")
Retrieving mesh output data
---------------------------
Internally RenderDoc refers to any general data fetched from any stage as "Post VS" data. Fetching this is fairly simple via a call to :meth:`~renderdoc.ReplayController.GetPostVSData` for a given stage and for a given instance and multiview.
Post VS data is cached per-event unless something causes the cache to be invalidated like shader editing. If you have the mesh viewer open in the UI then the data is already being cached whenever an event is selected.
.. warning::
As in other cases, this can take some time depending on if the cache is warm or not so it is best to consider ensuring this call happens on the :ref:`replay thread <pythreading>`!
This function returns a :class:`~renderdoc.MeshFormat` which details the properties of the returned data in an optional index buffer and vertex buffer. The index buffer is *not* necessarily the same as any index buffer used in the draw, and is not interchangeable. The buffer resources referred to are internal and will not match the IDs returned for any buffer in the capture itself.
.. highlight:: python
.. code:: python
controller = pyrenderdoc.GetBlockingController()
meshdata = controller.GetPostVSData(0, 0, renderdoc.MeshDataStage.VSOut)
The :class:`~renderdoc.MeshFormat` also contains information about the size of the draw and its topology, in case this stage is not outputting triangles to the rasterizer. For stages that do rasterize their output RenderDoc estimates the projection matrix's near plane and far plane which can be used to display the unprojected mesh data.
.. highlight:: python
.. code:: python
print(f"Mesh data contains {meshdata.numIndices} indices in {str(meshdata.topology)}")
if meshdata.indexResourceId != renderdoc.ResourceId():
print(" (indexed)")
else:
print(" (non-indexed)")
if meshdata.unproject:
print(f" Rasterized data: {meshdata.nearPlane:.2f}-{meshdata.farPlane:.2f}")
Fetching indices
----------------
To decode the provided vertex data we first set up a list of indices. If there is an index buffer we can fetch its buffer contents with :meth:`~renderdoc.ReplayController.GetBufferData` and decode using ``struct.unpack``. If there's no index buffer it's simple and we can generate a list of integers ourselves.
The details of unpacking formats are available in the `python documentation <https://docs.python.org/3/library/struct.html>`_, but we only need to handle a couple of different possible index byte sizes as determined by :data:`~renderdoc.MeshFormat.indexByteStride`.
.. highlight:: python
.. code:: python
idxs = [i for i in range(meshdata.numIndices)]
if meshdata.indexResourceId != renderdoc.ResourceId():
bufdata = controller.GetBufferData(
meshdata.indexResourceId, meshdata.indexByteOffset, meshdata.indexByteSize
)
# 01234
struct_type = " BH I"[meshdata.indexByteStride]
idxs = cast(
List[int], struct.unpack_from(f"={meshdata.numIndices}{struct_type}", bufdata)
)
Fetching vertex data
--------------------
The new vertex buffer generated by RenderDoc will have a format closely following the :data:`~renderdoc.ShaderReflection.outputSignature` from the reflection data of the stage that output it - in our case the vertex shader.
As a general rule, the output data follows a specific format - each vertex is separated by a stride of :data:`~renderdoc.MeshFormat.vertexByteStride` bytes and starts at :data:`~renderdoc.MeshFormat.vertexByteOffset` in the vertex buffer. The vertex data is made up of the reflection's output signature elements, including any builtin outputs.
We will iterate over up to 4 triangles, and in each triangle process each index. For each index we use it to calculate the offset in the vertex buffer and fetch the data for the whole vertex:
.. highlight:: python
.. code:: python
idx += meshdata.baseVertex
offset = meshdata.vertexByteOffset + meshdata.vertexByteStride * idx
vert_data = controller.GetBufferData(
meshdata.vertexResourceId, offset, meshdata.vertexByteStride
)
.. tip::
It would be better to fetch the buffer data for all vertices into python at once and then slice it here, but for this example we query the buffer data per-vertex
Decoding vertex data
--------------------
We can now decode the vertex data according to the expected layout, but there are two important points to note:
#. For shaders that are the last stage before the rasterizer and output to the builtin position (:data:`~renderdoc.ShaderBuiltin.Position`) this position data is always output first in the vertex data, before every other element in order.
This re-ordering is done by RenderDoc so that the mesh data returned by :meth:`~renderdoc.ReplayController.GetPostVSData` immediately describes the position data. In many cases there is no re-ordering as position is often the first declared output anyway.
#. By default all data is tightly packed without respect for alignment. However on some APIs and shader stages, each element will be aligned up according to 'traditional' conservative padding: with vector elements aligned so they do not cross a 16-byte boundary.
This can be queried with :meth:`~renderdoc.PipeState.HasAlignedPostVSData`.
In our example we will handle both of these for demonstration, though you may find in your capture that one or both is redundant.
First we identify the position output. Since we know that this draw only uses a vertex shader so it must write to position. We can then decode the position, assuming it is float data but fetching the number of components from the output signature.
.. highlight:: python
.. code:: python
posidx = [
o.systemValue == renderdoc.ShaderBuiltin.Position
for o in refl.outputSignature
].index(True)
if posidx >= 0:
pos = refl.outputSignature[posidx]
# simple case, we assume float output and don't have to worry about alignment
posdata = struct.unpack_from(f"={pos.compCount}f", vert_data)
print(f" <pos>: {fmt_vec(posdata)}")
After the position will follow all of the other signature elements in the order they appear in the reflection signature. We track this in an ``offset`` variable which we update after fetching the position.
Before each element we check if we need to align upwards for the new element's data:
.. highlight:: python
.. code:: python
for output in refl.outputSignature:
# position was handled above, so skip it here
if output.systemValue == renderdoc.ShaderBuiltin.Position:
continue
if pipe.HasAlignedPostVSData(renderdoc.MeshDataStage.VSOut):
align = max(4, renderdoc.VarTypeByteSize(output.varType))
if output.compCount == 3:
align *= 4
else:
align *= output.compCount
if offset % align != 0:
offset = align - (offset % align)
data_offs = offset
offset += output.compCount * renderdoc.VarTypeByteSize(output.varType)
Once we know where the data appears, we can decode it. For simplicity we will only handle 32-bit integer and floating point data, which covers most common types as this includes vectors.
.. highlight:: python
.. code:: python
fmtchar = ""
if output.varType == renderdoc.VarType.Float:
fmtchar = "f"
elif output.varType == renderdoc.VarType.UInt:
fmtchar = "I"
elif output.varType == renderdoc.VarType.SInt:
fmtchar = "i"
if fmtchar != "":
fmt = f"={output.compCount}{fmtchar}"
data = fmt_vec(struct.unpack_from(fmt, vert_data, data_offs))
else:
data = "<non-decoded data>"
Putting this all together we can then print this signature's data for that vertex:
.. highlight:: python
.. code:: python
name = output.varName
if name == "":
name = output.semanticIdxName
print(f" {name}: {fmt_vec(data)}")
Sample Output
-------------
.. sourcecode:: text
Mesh data contains 36 indices in Topology.TriangleList
(non-indexed)
Rasterized data: 0.20-100.00
Triangle 0:
[0]:
<pos>: -0.416, 3.814, 4.952, 5.142
texcoord: 0.000, 1.000, 0.000, 0.000
frag_pos: -0.416, 3.814, 4.952
[1]:
<pos>: 3.389, 2.284, 6.010, 6.198
texcoord: 1.000, 1.000, 0.000, 0.000
frag_pos: 3.389, 2.284, 6.010
[2]:
<pos>: 3.389, -1.856, 4.979, 5.169
texcoord: 1.000, 0.000, 0.000, 0.000
frag_pos: 3.389, -1.856, 4.979
Triangle 1:
[3]:
<pos>: 3.389, -1.856, 4.979, 5.169
texcoord: 1.000, 0.000, 0.000, 0.000
frag_pos: 3.389, -1.856, 4.979
[4]:
<pos>: -0.416, -0.327, 3.921, 4.113
texcoord: 0.000, 0.000, 0.000, 0.000
frag_pos: -0.416, -0.327, 3.921
[5]:
<pos>: -0.416, 3.814, 4.952, 5.142
texcoord: 0.000, 1.000, 0.000, 0.000
frag_pos: -0.416, 3.814, 4.952
Triangle 2:
[6]:
<pos>: -0.416, 3.814, 4.952, 5.142
texcoord: 1.000, 1.000, 0.000, 0.000
frag_pos: -0.416, 3.814, 4.952
[7]:
<pos>: -3.389, -2.284, 5.275, 5.464
texcoord: 0.000, 0.000, 0.000, 0.000
frag_pos: -3.389, -2.284, 5.275
[8]:
<pos>: -3.389, 1.856, 6.306, 6.493
texcoord: 0.000, 1.000, 0.000, 0.000
frag_pos: -3.389, 1.856, 6.306
Triangle 3:
[9]:
<pos>: -0.416, 3.814, 4.952, 5.142
texcoord: 1.000, 1.000, 0.000, 0.000
frag_pos: -0.416, 3.814, 4.952
[10]:
<pos>: -0.416, -0.327, 3.921, 4.113
texcoord: 1.000, 0.000, 0.000, 0.000
frag_pos: -0.416, -0.327, 3.921
[11]:
<pos>: -3.389, -2.284, 5.275, 5.464
texcoord: 0.000, 0.000, 0.000, 0.000
frag_pos: -3.389, -2.284, 5.275
Example Source
--------------
This example can be found under the name "Mesh Output" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <mesh_output.py>`.
.. literalinclude:: mesh_output.py
+177
View File
@@ -0,0 +1,177 @@
Example: Pipeline State
=======================
The pipeline state abstraction RenderDoc provides gives simple API-agnostic access to the most common states and bindings at a given event. In this example we will see a couple of the things that can be queried to give ideas of how this can be applied in your own scripts.
Output bindings
---------------
To begin with we will fetch the pipeline state (:meth:`~qrenderdoc.CaptureContext.CurPipelineState`) at the :ref:`currentevent` and declare a helper function for looking up names to reduce verbosity of later code.
We'll then print out the output targets (:meth:`~renderdoc.PipeState.GetOutputTargets`) and depth target (:meth:`~renderdoc.PipeState.GetDepthTarget`) for the current draw. This may be empty depending on the current state at the event you run this script. The exact number of entries returned may also depend on API-specific details and how targets are bound, so we skip any 'unbound' targets.
.. highlight:: python
.. code:: python
pipe = pyrenderdoc.CurPipelineState()
get_name = lambda id: pyrenderdoc.GetResourceName(id)
outs = pipe.GetOutputTargets()
for i, out in enumerate(outs):
id = out.resource
if id != renderdoc.ResourceId():
print(f"Out {i}: {get_name(id)}")
id = pipe.GetDepthTarget().resource
print(f"Depth: {get_name(id)}")
Pipeline objects and shaders
----------------------------
We can also query for the shaders and pipeline that are bound here. Again depending on the API you are using there may not be such a thing as a pipeline object, but shaders will be present regardless of whether PSOs are used or not.
.. highlight:: python
.. code:: python
id = pipe.GetGraphicsPipelineObject()
print(f"Pipeline: {get_name(id)}")
id = pipe.GetShader(renderdoc.ShaderStage.Vertex)
print(f"VS: {get_name(id)}")
id = pipe.GetShader(renderdoc.ShaderStage.Pixel)
print(f"PS: {get_name(id)}")
.. note::
Although OpenGL has a concept of a 'pipeline' as well as programs and shaders, this is not considered to be a true pipeline state object (PSO) and so will not be listed here. Only the OpenGL-specific pipeline state in RenderDoc will show these bindings, which are largely opaque.
This can also be a useful point to query the current shader reflection (see :doc:`shader_refl`) via :meth:`~renderdoc.PipeState.GetShaderReflection` and dig in deeper to the declared bindings and shader information.
Shader Binding Helpers
----------------------
Accessing shader bindings can be quite involved, as this is an area where APIs can differ quite significantly and RenderDoc's abstraction must be more complex to allow easier access. First we will look at the highest level helper, which is very abstracted but will cover many common and simple uses.
The pipeline state abstraction offers several queries for obtaining different types of resource bindings by shader stage.
As outlined in :doc:`../in_depth/shader_refl` (as well as in :doc:`in more detailed write-ups <../in_depth/descriptors_bindings>`) RenderDoc classifies bindings into four broad categories - constant blocks, samplers, read-only resources and read-write resources.
Here we will query for the constant blocks bound to the vertex shader, and print out the buffer that is bound. Using shader reflection (see :doc:`shader_refl`) we could also use the known stage + index to look up in the reflection information how this buffer binding is used.
.. highlight:: python
.. code:: python
cbs = pipe.GetConstantBlocks(renderdoc.ShaderStage.Vertex)
for cb in cbs:
print(
f"{str(cb.access.stage)} CB[{cb.access.index}]: {get_name(cb.descriptor.resource)}"
)
Within the :data:`~renderdoc.Descriptor` RenderDoc also lists more information, for constant blocks this may be a relative byte offset (:data:`~renderdoc.Descriptor.byteOffset`) where the binding starts, for texture access this could include the mips accessible (:data:`~renderdoc.Descriptor.firstMip`), format-cast (:data:`~renderdoc.Descriptor.format`), or component swizzle (:data:`~renderdoc.Descriptor.swizzle`).
Direct descriptor information
-----------------------------
In most cases looking up bindings via the helper above will be sufficient for knowing which resources are accessed, but it is possible to get more unfiltered information.
To do this we will query for a list of all descriptors (:meth:`~renderdoc.PipeState.GetAllUsedDescriptors`), and ask for only those which are actually known to be used. On some APIs it is possible to also query for descriptors which are bound but known to be unused - e.g. because the shader does not use them. Generally this distinction is only present for non-bindless style APIs where the set of possible bindings is a relatively small and fixed set.
.. highlight:: python
.. code:: python
descs = pipe.GetAllUsedDescriptors(True)
Next we will examine this list on two different axes - printing resources of all types used by a given shader stage, and printing all descriptors of a given type used by all shader stage.
We also print out the descriptor store and offset where this came from, which can be used for more detailed analysis if desired. Bear in mind that this requires understanding how RenderDoc structures its :doc:`abstraction <../in_depth/descriptors_bindings>`.
.. highlight:: python
.. code:: python
for stage in renderdoc.ShaderStage:
stage_descs = [d for d in descs if d.access.stage == stage]
if stage_descs == []:
continue
print(f"** {str(stage)} descriptors:")
for d in stage_descs:
desc_str = f"{str(d.access.type)} - "
if (
d.sampler.object != renderdoc.ResourceId()
and d.descriptor.resource != renderdoc.ResourceId()
):
desc_str += (
f"{get_name(d.descriptor.resource)} + {get_name(d.sampler.object)}"
)
elif d.sampler.object != renderdoc.ResourceId():
desc_str += f"{get_name(d.sampler.object)}"
else:
desc_str += f"{get_name(d.descriptor.resource)}"
print(desc_str)
print(
f" in {get_name(d.access.descriptorStore)} at offset {d.access.byteOffset}"
)
Sample Output
-------------
.. sourcecode:: text
-------------------------
Outputs
-------------------------
Out 0: Swapchain Image 127
Depth: 2D Depth Attachment 148
-------------------------
Pipeline/Shaders
-------------------------
Pipeline: Graphics Pipeline 112
VS: Shader Module 109
PS: Shader Module 110
-------------------------
Constant Blocks (VS)
-------------------------
ShaderStage.Vertex CB[0]: Buffer 100
-------------------------
Descriptors by Stage
-------------------------
** ShaderStage.Vertex descriptors:
DescriptorType.ConstantBuffer - Buffer 100
in Descriptor Set 118 at offset 0
** ShaderStage.Pixel descriptors:
DescriptorType.ImageSampler - 2D Image 95 + Sampler 98
in Descriptor Set 118 at offset 1
-------------------------
Descriptors by Type
-------------------------
** DescriptorType.ConstantBuffer descriptors:
ShaderStage.Vertex - Buffer 100
in Descriptor Set 118 at offset 0
** DescriptorType.ImageSampler descriptors:
ShaderStage.Pixel - 2D Image 95 + Sampler 98
in Descriptor Set 118 at offset 1
API-specific pipelines
----------------------
Although not shown in this example, it is also possible to query the pipeline state for each API. This will naturally only be available if the capture open is actually using that API.
Fetching data through the API-specific pipeline structure may be necessary if you are doing something that is very API specific and needs precise details, or if you want to examine data which is not common and shared by all APIs and is only present on some.
Example Source
--------------
This example can be found under the name "Pipeline State" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <pipe_state.py>`.
.. literalinclude:: pipe_state.py
@@ -0,0 +1,91 @@
Example: Resource Usage
=======================
When loading a capture, RenderDoc stores a limited amount of information about the global use of resources across the whole frame. This can then be queried so that you can know what events a texture is used in without having to select every event and check the current pipeline state bindings.
In this example we will show how this can be used to track the usage of a buffer and a texture relative to the :ref:`currentevent`.
Selecting the resources
-----------------------
First we will choose which resources we want to track the usage for. To keep things simple we will look at fixed bindings that are likely to be commonly used at a normal draw - the index buffer (:meth:`~renderdoc.PipeState.GetIBuffer`) and the depth target (:meth:`~renderdoc.PipeState.GetDepthTarget`). If one or both of these are unbound we will throw an error to avoid needing to error-check later on.
We also need to obtain the :class:`~renderdoc.ReplayController` to query the usage information. As in other examples for simplicity we use :meth:`~qrenderdoc.CaptureContext.GetBlockingController` to obtain a blocking version of the :class:`~renderdoc.ReplayController`. Although this does block, we expect usage queries to be fast so it has minimal impact but it is worth noting that this could be done on a different thread to be truly asynchronous - see :ref:`pythreading`.
.. highlight:: python
.. code:: python
pipe = pyrenderdoc.CurPipelineState()
depth = pipe.GetDepthTarget().resource
ib = pipe.GetIBuffer().resourceId
if depth == renderdoc.ResourceId() or ib == renderdoc.ResourceId():
raise RuntimeError(
"Can't run example!\n"
"Current event doesn't use both index buffer and depth target"
)
eid = pyrenderdoc.CurEvent()
controller = pyrenderdoc.GetBlockingController()
Querying usage list
-------------------
We will loop over both resources since the querying for usage is agnostic and we will not be looking for anything resource-specific but just looking at the list of usage entries. When calling :meth:`~renderdoc.ReplayController.GetUsage` you pass the :ref:`Resource ID <resourceids>` of the resource and it will return a list of :class:`~renderdoc.EventUsage` in order of ascending :ref:`event ID <eventids>` and giving the :class:`~renderdoc.ResourceUsage` at each event where the resource is used.
If there is only one entry and it is at :ref:`event ID <eventids>` 0 with usage :data:`~renderdoc.ResourceUsage.Unused` then this resource type was not tracked during loading and no data is available. If the list is empty, that means the resource was never used - in our case this is impossible as we know it was used at least at the current event so we look up the :class:`~renderdoc.ResourceUsage` for the current event by filtering the list.
.. highlight:: python
.. code:: python
for name, id in [("Depth Target", depth), ("Index Buffer", ib)]:
usagelist = controller.GetUsage(id)
cur_usage = next(u for u in usagelist if u.eventId == eid).usage
Finding adjacent usage
----------------------
We will now look before and after the current event for the next usage entry which has a *different* :class:`~renderdoc.ResourceUsage`. There will be one usage entry per event so it is quite likely to find series of several events in the same pass where the resource is used in the same way.
.. highlight:: python
.. code:: python
prev_usages = [u for u in usagelist if u.eventId < eid and u.usage != cur_usage]
later_usages = [u for u in usagelist if u.eventId > eid and u.usage != cur_usage]
Because we don't know which event is currently selected and where else the resource is used, either of these lists may be empty. If they are empty we will print a message indicating so, otherwise we will print the last previous usage, or the first later usage.
.. highlight:: python
.. code:: python
if len(prev_usages) == 0:
print(f"{name} {pyrenderdoc.GetResourceName(id)} was never used before {eid}!")
else:
print(
f"{name} {pyrenderdoc.GetResourceName(id)} was used as "
f"{str(prev_usages[-1].usage)} at {str(prev_usages[-1].eventId)}."
)
Sample Output
-------------
.. sourcecode:: text
Depth Target GBufferDepth was used as ResourceUsage.Clear at 1347.
Depth Target GBufferDepth will be used as ResourceUsage.Barrier at 1516.
Index Buffer MeshIndices was used as ResourceUsage.CS_RWResource at 1390.
Index Buffer MeshIndices will be used as ResourceUsage.CS_RWResource at 2065.
Example Source
--------------
This example can be found under the name "Resource Usage" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <resource_usage.py>`.
.. literalinclude:: resource_usage.py
+201
View File
@@ -0,0 +1,201 @@
Example: Shader Reflection
==========================
From the :doc:`Pipeline state <pipe_state>` you can obtain the currently bound shader reflection for a given stage - or if you have the ID and entry point for a particular shader you can query it directly with :meth:`~renderdoc.ReplayController.GetShader`.
In this example we will examine what information is available via the :doc:`shader reflection <../in_depth/shader_refl>`.
Input & Output signatures
-------------------------
First we will fetch both the vertex and pixel shaders (:meth:`~renderdoc.PipeState.GetShaderReflection`). We assume these are present, and throw an error if they aren't - e.g. because a non-draw is selected or a draw that doesn't use both shaders.
.. highlight:: python
.. code:: python
vs = pyrenderdoc.CurPipelineState().GetShaderReflection(renderdoc.ShaderStage.Vertex)
ps = pyrenderdoc.CurPipelineState().GetShaderReflection(renderdoc.ShaderStage.Pixel)
if vs is None or ps is None:
raise ValueError("Expected a draw with a VS and PS to be selected")
From here we will look at the input and output signatures (:data:`~renderdoc.ShaderReflection.inputSignature` and :data:`~renderdoc.ShaderReflection.outputSignature`) for the vertex shader. The input signature will contain any special :class:`~renderdoc.ShaderBuiltin` elements such as (:data:`~renderdoc.ShaderBuiltin.VertexIndex`) or (:data:`~renderdoc.ShaderBuiltin.InstanceIndex`), as well as any fixed function vertex inputs declared. Similarly the output values will typically contain both special values such as position, as well as user-defined values to be interpolated and passed through to the pixel shader.
.. warning::
RenderDoc identifies special inputs like this using :class:`~renderdoc.ShaderBuiltin` but *does not* dictate an interpretation. In some cases the meaning of these may vary by API - for example being either always 0-indexed or offset by draw parameters like ``firstVertex`` or ``vertexOffset``.
The reflection for input and output signature values may vary between APIs, and so two possible name are used. If available we use the variable name (:data:`~renderdoc.SigParameter.varName`) which is the most likely to be relevant. If there is no reflected variable name such as on D3D APIs, we instead use the semantic name combined with any semantic index (:data:`~renderdoc.SigParameter.semanticIdxName`).
We can query their type (:data:`~renderdoc.SigParameter.varType`) and vector component count (:data:`~renderdoc.SigParameter.compCount`). The configuration of where data for fixed function vertex inputs are sourced from can be queried via :class:`~renderdoc.PipeState.GetVertexInputs`, indexed by :data:`~renderdoc.SigParameter.regIndex`. For vertex outputs this information can be used to decode mesh output data (see :doc:`mesh_output`) as it is laid out according to the output signature.
.. highlight:: python
.. code:: python
for vin in vs.inputSignature:
name = vin.varName
if name == "":
name = vin.semanticIdxName
print(
f"Vertex input {name} is {str(vin.varType)} x {vin.compCount} "
f"at register {vin.regIndex}"
)
for vout in vs.outputSignature:
name = vout.varName
if name == "":
name = vout.semanticIdxName
print(
f"Vertex input {name} is {str(vout.varType)} x {vout.compCount} "
f"at register {vout.regIndex}"
)
Constant Block Bindings
-----------------------
The reflection information contains information about each type of binding a shader can have - in RenderDoc these are categorised into constant blocks, read-only resources, read-write resources and samplers. This may slightly vary from how each API treats bindings - see :doc:`../in_depth/shader_refl`.
For constant blocks, we can find out properties like the name (:data:`~renderdoc.ConstantBlock.name`) of the constant block (if available), its byte size (:data:`~renderdoc.ConstantBlock.byteSize`), and which bind point it is bound to. This bind point is API specific and is not used elsewhere in RenderDoc but can be convenient for user display and interpreted in an API-specific manner. For more information see :doc:`../in_depth/descriptors_bindings`.
We can also inspect the reflection if available to see the names and types of the shader variables declared in this constant block (:data:`~renderdoc.ConstantBlock.variables`). This is a recursive listing of structures, arrays, and basic values like scalars, vectors, and matrices. For example we print out the first variable and its type.
.. tip::
If you want to decode and find the contents of variables in a constant block it may be helpful to use the :meth:`~renderdoc.ReplayController.GetCBufferVariableContents` function which will handle the details of interpreting these variables into a given structure with all values available in-line.
This also handles the case where the constant block is not sourced from a buffer and its contents are not available directly.
.. highlight:: python
.. code:: python
if len(vs.constantBlocks) > 0:
cb = vs.constantBlocks[0]
print(
f" First is named {cb.name} "
f"at {cb.fixedBindSetOrSpace}:{cb.fixedBindNumber}"
)
if cb.compileConstants:
print(" (compile-time constants)")
elif not cb.bufferBacked:
print(" (runtime non-buffer temp data)")
else:
print(f" (from a buffer, expected {cb.byteSize} bytes)")
print(f" containing {len(cb.variables)} variables")
if len(cb.variables) > 0:
var = cb.variables[0]
print(f" the first is named {var.name} at offset {var.byteOffset}")
print(
f" type {str(var.type.baseType)} "
f"dimension {var.type.rows}x{var.type.columns}"
)
Texture & Sampler Bindings
--------------------------
Similarly to the constant blocks above, you can query which read-only resources (including read-only textures) and sampler bindings a shader has.
Most useful data is stored in the descriptor itself not in shader reflection, but in the binding you can the expected resource type (:data:`~renderdoc.ShaderResource.textureType`) and format (:data:`~renderdoc.ShaderResource.variableType`) which APIs typically require to match the descriptor.
For some APIs you may find a resource type that is a combined image and sampler. In this case you will not see a sampler binding at all, and the texture itself will have marked that it contains an embedded/combined sampler (:data:`~renderdoc.ShaderResource.hasSampler`).
.. highlight:: python
.. code:: python
print(f"PS has {len(ps.readOnlyResources)} R/O resources")
if len(ps.readOnlyResources) > 0:
res = ps.readOnlyResources[0]
print(
f" First is named {res.name} "
f"at {res.fixedBindSetOrSpace}:{res.fixedBindNumber}"
)
print(f" declared as {str(res.textureType)} of {res.variableType.baseType}")
if res.hasSampler:
print(f" ++ has attached sampler")
print(f"PS has {len(ps.samplers)} samplers")
if len(ps.samplers) > 0:
samp = ps.samplers[0]
print(
f" First is named {samp.name} "
f"at {samp.fixedBindSetOrSpace}:{samp.fixedBindNumber}"
)
Debug Information
-----------------
The shader reflection can also contain optional debug information (:class:`~renderdoc.ShaderDebugInfo`) if the shader was compiled with it. Much of this information will not be available if it was stripped or never generated by the compiler, so care should be taken not to assume things will be set.
In our example we try to print out some metadata about what language the shader was compiled (:data:`~renderdoc.ShaderDebugInfo.encoding`) from and with which tool (:data:`~renderdoc.ShaderDebugInfo.compiler`), as well as whether or not it can be debugged (:data:`~renderdoc.ShaderDebugInfo.debuggable`). More debug information is available here including the source code itself (:data:`~renderdoc.ShaderDebugInfo.files`).
.. highlight:: python
.. code:: python
print(f"PS was compiled by {renderdoc.ToolExecutable(ps.debugInfo.compiler)}")
print(f"{str(ps.debugInfo.encoding)} was compiled to {str(ps.encoding)}")
if ps.debugInfo.debuggable:
print("PS is debuggable!")
else:
print(f"PS can't be debugged: {ps.debugInfo.debugStatus}")
Shader Disassembly
------------------
Shader disassembly is not directly available in the shader reflection because there are multiple possible disassembly formats and because generating and storing the disassembly costs enough to be done by default.
Fetching disassembly is done via :meth:`~renderdoc.ReplayController.DisassembleShader`, which requires the shader reflection object. Some disassembly formats will also expect the pipeline ID if the API uses pipeline objects - omitting this is possible, but disassembly may fail or may produce slightly different results depending on the circumstances. This will return a string either containing the disassembly in the requested format, or an error if the disassembly process failed.
If an empty string is passed as the disassembly format, RenderDoc will use its default disassembly. This varies depending on API but is what is considered the most readable form of the direct shader representation - DXBC, DXIL or SPIR-V depending on the API.
Other formats available can be enumerated via :meth:`~renderdoc.ReplayController.GetDisassemblyTargets` which returns a list of strings, each string being a name of a disassembly target.
.. warning::
:meth:`~renderdoc.ReplayController.GetDisassemblyTargets` takes a parameter ``withPipeline`` which if set to ``True`` will include disassembly formats that *require* a pipeline object. It is still true that other disassembly formats may not produce 100% accurate results if the pipeline object is available but omitted.
Sample Output
-------------
.. sourcecode:: text
Vertex input gl_VertexIndex is VarType.SInt x 1 at register 0
Vertex input gl_Position is VarType.Float x 4 at register 0
Vertex input texcoord is VarType.Float x 4 at register 0
Vertex input frag_pos is VarType.Float x 3 at register 1
VS has 1 constant blocks declared
First is named ubuf at 0:0
(from a buffer, expected 1216 bytes)
containing 3 variables
the first is named MVP at offset 0
type VarType.Float dimension 4x4
PS has 1 R/O resources
First is named tex at 0:1
declared as TextureType.Texture2D of 0
++ has attached sampler
PS has 0 samplers
PS was compiled by glslangValidator
ShaderEncoding.GLSL was compiled to ShaderEncoding.SPIRV
PS is debuggable!
Example Source
--------------
This example can be found under the name "Shader Reflection" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <shader_refl.py>`.
.. literalinclude:: shader_refl.py
+98
View File
@@ -0,0 +1,98 @@
Example: Show buffer with format
================================
This example shows how to fetch the information and contents of a buffer, as well as opening up a view of it in the UI.
This could for example be run as a command line argument when starting the UI, to avoid repetitive steps or automate a repro case.
Fetching Buffer Metadata
------------------------
First we iterate through the list of buffers (:meth:`~qrenderdoc.CaptureContext.GetBuffers`) to find the one we want. The selection criteria would be up to you, in this case we look at the buffer's name (:meth:`~qrenderdoc.CaptureContext.GetResourceName`) and choose a buffer using that - however it could also be a particular size, or the buffer :doc:`bound to a shader <shader_refl>` at a given event. To keep the example flexible we will default to using the last buffer in the list if one doesn't match the criteria.
.. tip::
If you already have the :ref:`Resource ID <resourceids>` of the buffer you want, you can use :meth:`~qrenderdoc.CaptureContext.GetBuffer` to fetch the descriptor for it.
.. highlight:: python
.. code:: python
mybuf = renderdoc.ResourceId.Null()
for buf in pyrenderdoc.GetBuffers():
print(f"buf {buf.resourceId} is {pyrenderdoc.GetResourceName(buf.resourceId)}")
mybuf = buf.resourceId
# here put your actual selection criteria - i.e. look for a particular name
if "Vertex" in pyrenderdoc.GetResourceName(buf.resourceId):
break
print(f"selected {pyrenderdoc.GetResourceName(mybuf)}")
Opening Buffer Viewer
---------------------
Once we've identified the buffer we want to view, we create a buffer viewer (:meth:`~qrenderdoc.CaptureContext.ViewBuffer`) and display it on the main tool area (:meth:`~qrenderdoc.CaptureContext.AddDockWindow`).
.. highlight:: python
.. code:: python
formatter = """
float3 pos;
half norms[6];
uint flags;
"""
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)
.. figure:: ../../imgs/python/BufferViewer.png
The buffer viewer we opened for the buffer we chose.
Fetching Buffer Contents
------------------------
Lastly we'll go a step further and fetch the buffer data (:meth:`~renderdoc.ReplayController.GetBufferData`) ourselves to print the first 8 bytes. To access this we will need to obtain the :class:`~renderdoc.ReplayController` which controls RenderDoc's underlying analysis.
For convenience we will fetch a blocking version (:meth:`~qrenderdoc.CaptureContext.GetBlockingController`) that stalls the python script and executes the given command. If this code ran in a UI extension that could cause the UI to become unresponsive while the buffer data is fetched so this work could be done on a thread instead - see :ref:`pythreading`.
.. note::
As with most data retrieved from RenderDoc, this buffer data is relative to the :ref:`current event <currentevent>` - the same as if a buffer viewer is opened in the UI. Changing to a different current event may mean different data is fetched and printed.
With the replay controller we can request a given byte range by its offset and length. If we wanted to get the whole buffer we could specify a length of 0. This is returned as a python ``bytes`` object which encapsulates a raw byte sequence, and ``struct.unpack_from`` is a python function that interprets bytes into values - see the `python documentation <https://docs.python.org/3/library/struct.html>`_ for how to write format strings to pull out floats and different byte-width values.
.. highlight:: python
.. code:: python
controller = pyrenderdoc.GetBlockingController()
data_bytes = controller.GetBufferData(mybuf, 0, 8)
data_decoded = struct.unpack_from("8B", data_bytes)
print(f"The first 8 bytes of the buffer are: {data_decoded}")
Final output from the script with this decoding:
.. sourcecode:: text
buf ResourceId::111 is Buffer 111
selected Buffer 111
The first 8 bytes of the buffer are: (69, 64, 190, 191, 12, 146, 122, 191)
Example Source
--------------
This example can be found under the name "Show buffer with format" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <show_buffer.py>`.
.. literalinclude:: show_buffer.py
+128
View File
@@ -0,0 +1,128 @@
Example: Show and save a texture
================================
This example demonstrates how to enumerate textures, show one in the texture viewer, and save the texture to disk.
Fetching Texture Metadata
-------------------------
First we iterate through the list of textures (:meth:`~qrenderdoc.CaptureContext.GetTextures`) and print their dimensions as we go. We keep track of which texture has the largest area.
.. highlight:: python
.. code:: python
highestArea = 0
largest = None
for tex in pyrenderdoc.GetTextures():
name = pyrenderdoc.GetResourceName(tex.resourceId)
print(f"{name} is {tex.width} x {tex.height}")
area = tex.width * tex.height
if area > highestArea:
highestArea = area
largest = tex
Opening in Texture Viewer
-------------------------
Once we've found the largest texture, we print its information again as a summary and then show (:meth:`~qrenderdoc.CaptureContext.ShowTextureViewer`) and ask the :class:`~qrenderdoc.TextureViewer` to display it as a new locked tab (:meth:`~qrenderdoc.TextureViewer.ViewTexture`).
.. highlight:: python
.. code:: python
if largest is not None:
name = pyrenderdoc.GetResourceName(largest.resourceId)
print(f"\n+++ Largest texture is {name}")
# open largest texture (by area) in texture viewer, and focus
pyrenderdoc.ShowTextureViewer()
pyrenderdoc.GetTextureViewer().ViewTexture(largest.resourceId,
renderdoc.CompType.Typeless,
True)
.. figure:: ../../imgs/Screenshots/CurrentVsLockedTab.png
An example locked tab that has been opened from the python script.
To go further we will now save this texture to disk in a couple of different formats.
Saving Texture to Disk
----------------------
We will need to obtain the :class:`~renderdoc.ReplayController` which controls RenderDoc's underlying analysis.
.. tip::
Although not shown in this example, with the texture ID you can use :meth:`~renderdoc.ReplayController.GetTextureData` to fetch the raw bytes for a given subresource in a texture, for arbitrary processing.
For convenience we will fetch a blocking version (:meth:`~qrenderdoc.CaptureContext.GetBlockingController`) that stalls the python script and executes the given command. If this code ran in a UI extension that could cause the UI to become unresponsive while the texture is processed and written to disk so this work could be done on a thread instead - see :ref:`pythreading`.
.. highlight:: python
.. code:: python
controller = pyrenderdoc.GetBlockingController()
Next so that we know where to save the file, we prompt the user to browse to a filename (:meth:`qrenderdoc.ExtensionManager.SaveFileName`). We'll replace the extension so trim off any ``.jpg`` we get.
.. highlight:: python
.. code:: python
filename = pyrenderdoc.Extensions().SaveFileName(
"Choose where to save JPG/PNG/DDS texture files", "", "*.jpg"
)
filename = filename.replace(".jpg", "")
Saving textures to disk can require a few different configuration options, which is contained in the :class:`~renderdoc.TextureSave` configuration structure.
Not all textures map cleanly to normal texture formats and some textures may have multiple mips or array slices. To start with we will specify that when writing a texture format without an alpha channel RenderDoc should blend to a checkerboard pattern (:data:`~renderdoc.AlphaMapping.BlendToCheckerboard`). We also choose to save mip 0 if there are multiple mips, and if there are multiple slices save only slice 0. Other options are possible to e.g. lay out all slices in a grid atlas.
.. highlight:: python
.. code:: python
texsave = renderdoc.TextureSave()
texsave.resourceId = largest.resourceId
# Blend alpha to a checkerboard pattern for formats without alpha support
texsave.alpha = renderdoc.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
With that done we can save the texture in both :data:`~renderdoc.FileType.JPG` and :data:`~renderdoc.FileType.PNG` formats with a call to :meth:`~renderdoc.ReplayController.SaveTexture`.
.. highlight:: python
.. code:: python
texsave.destType = renderdoc.FileType.JPG
controller.SaveTexture(texsave, filename + ".jpg")
# For formats with an alpha channel, preserve it
texsave.alpha = renderdoc.AlphaMapping.Preserve
texsave.destType = renderdoc.FileType.PNG
controller.SaveTexture(texsave, filename + ".png")
Finally we will save to :data:`~renderdoc.FileType.DDS`, and in this case we now have a texture format that can support mips and array slices. We'll change the configuration to ensure that all mips and all array slices are written to the same file.
.. code:: python
# 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 = renderdoc.FileType.DDS
controller.SaveTexture(texsave, filename + ".dds")
Example Source
--------------
This example can be found under the name "Show and save a texture" in the python scripting window.
.. only:: html and not htmlhelp
:download:`Download the example script <show_texture.py>`.
.. literalinclude:: show_texture.py