Files
renderdoc/docs/python_api/examples/history_debug.py
T
baldurk 83c53890da Mark accessors/return values that can be None with Optional[]
* This helps with type checkers to know that things can be None or not (both to
  silence previous warnings about "unnecessary" checks that are actually
  necessary, and to ensure those checks aren't omitted by accident)
2026-09-11 15:04:03 +01:00

212 lines
6.7 KiB
Python

# these imports are not strictly necessary, but are convenient
import renderdoc
import qrenderdoc
# this is here to give autocomplete when editing the example
# in VS Code where it doesn't know about this global
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pyrenderdoc = qrenderdoc.CaptureContext()
if not pyrenderdoc.IsCaptureLoaded():
filename = pyrenderdoc.Extensions().OpenFileName("Choose a capture", "", "*.rdc")
pyrenderdoc.LoadCapture(filename, renderdoc.ReplayOptions(), filename, False, True)
from typing import List
# a callback to repeatedly ask the user if they're ready, with a 5 second
# arbitrary wait each time they say no to give them a bit of time
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()
def prepare_history():
tex_view = pyrenderdoc.GetTextureViewer()
# find the selected texture and location
id = tex_view.GetCurrentResource()
sub = tex_view.GetSelectedSubresource()
x, y = tex_view.GetPickedLocation()
name = pyrenderdoc.GetResourceName(id)
print(f'Analysing on "{name}"@{renderdoc.DumpObject(sub)} at {x},{y}')
disp = renderdoc.TextureDisplay()
disp.subresource = sub
# show the window first, as the results will likely take some time to come back
history_window = pyrenderdoc.ViewPixelHistory(id, x, y, sub.slice, disp)
pyrenderdoc.AddDockWindow(
history_window.Widget(),
qrenderdoc.DockReference.RightOf,
pyrenderdoc.GetPythonShell().Widget(),
)
# get a blocking controller. This means long-running work like pixel history and
# shader debugging will block the running thread. In a UI extension this is not
# good but for python scripts it will block the script thread.
controller = pyrenderdoc.GetBlockingController()
history = controller.PixelHistory(id, x, y, sub, disp.typeCast)
history_window.SetHistory(history)
print(f"{len(history)} modifications to that pixel:")
for h in history:
col = lambda x: [int(c * 100.0) / 100.0 for c in x.col.floatValue]
if h.Passed():
print(
f" at EID {h.eventId} changed from {col(h.preMod)} to {col(h.postMod)}"
)
else:
print(f" at EID {h.eventId} modification failed")
# get a list of all drawcalls that passed with a pixel shader bound
eb = pyrenderdoc.GetEventBrowser()
passed_draws = list(
filter(
lambda x: eb.GetActionForEID(x.eventId).flags
& renderdoc.ActionFlags.Drawcall,
[h for h in history if h.Passed() and not h.unboundPS],
)
)
if len(passed_draws) == 0:
print("No draws wrote to this pixel with a pixel shader")
else:
p = passed_draws[0]
pyrenderdoc.SetEventID([], p.eventId, p.eventId, False)
pipe = pyrenderdoc.CurPipelineState()
refl = pipe.GetShaderReflection(renderdoc.ShaderStage.Pixel)
if refl is None or not refl.debugInfo.debuggable:
print("Shader can't be debugged:")
print(refl.debugInfo.debugStatus)
return
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
# the trace holds static global information about this debugged instance,
# such as the information of which resources are bound or reflected
# information per-instruction
states: List[renderdoc.ShaderDebugState] = []
# continually simulate the shader until it completes
while True:
more = controller.ContinueDebug(trace.debugger)
if more == []:
break
states += more
# 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()
infos = [
i for i in trace.instInfo if i.instruction <= state.nextInstruction
]
if infos == []:
info = trace.instInfo[0]
else:
info = infos[-1]
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}")
# if we want to display the shader debugger, we need to pass a new debugger
# as the UI wants to process the set of states itself
trace = controller.DebugPixel(x, y, inputs)
shad = pyrenderdoc.DebugShader(
refl,
pipe.GetGraphicsPipelineObject(),
trace,
"Debugged From Python",
)
pyrenderdoc.AddDockWindow(
shad.Widget(),
qrenderdoc.DockReference.BottomOf,
history_window.Widget(),
)
# start by calling our function that checks if the user is ready to debug
check_ready()