diff --git a/docs/python_api/examples/event_filter.py b/docs/python_api/examples/event_filter.py new file mode 100644 index 000000000..885bc1c04 --- /dev/null +++ b/docs/python_api/examples/event_filter.py @@ -0,0 +1,62 @@ +# 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) + +# 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 + + +# our main filter function. Could use the params or other things passed in +# to do more complex filtering +def filter_func( + ctx: qrenderdoc.CaptureContext, + filter: str, + params: str, + eventId: int, + chunk: renderdoc.SDChunk, + action: renderdoc.ActionDescription, + eventName: str, +): + return isprime(eventId) + + +# the parser gives us a chance to parse the params and cache the data, +# this is only called when the filter is modified. It also lets us +# error-check any params we want +def parser_func(ctx: qrenderdoc.CaptureContext, filter: str, params: str): + if "error" in params: + return f"You shouldn't put 'error' in ${filter}()" + return "" + + +# the completer can take the current params string and dynamically +# provide auto-complete suggestions for what to add. +def completer_func(ctx: qrenderdoc.CaptureContext, filter: str, params: str): + return ["foo", "bar", "error"] + + +# In a UI extension we'd register and unregister over the lifetime of the extension. +# But we can also just unregister unconditionally, and then re-register. Note if you +# do not unregister then this will fail and will refuse to overwrite an existing filter. +pyrenderdoc.GetEventBrowser().UnregisterEventFilterFunction("prime") +pyrenderdoc.GetEventBrowser().RegisterEventFilterFunction( + "prime", + "Show only events with prime EIDs.", + filter_func, + parser_func, + completer_func, +) diff --git a/docs/python_api/examples/exe_launching.py b/docs/python_api/examples/exe_launching.py new file mode 100644 index 000000000..11e343ec2 --- /dev/null +++ b/docs/python_api/examples/exe_launching.py @@ -0,0 +1,54 @@ +# 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() + +pyrenderdoc.ShowCaptureDialog() +dialog = pyrenderdoc.GetCaptureDialog() + +exe = pyrenderdoc.Extensions().OpenFileName("Find an executable", "", "*.exe") + +dialog.SetExecutableFilename(exe) + +dialog.SetCommandLine("--cool-level very") + +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) + +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() + + # don't allow the connection to close itself so we can expect that it will be valid + # when the delayed callback below is called + conn.PreventAutoClose() + + 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) diff --git a/docs/python_api/examples/history_debug.py b/docs/python_api/examples/history_debug.py new file mode 100644 index 000000000..7307134ea --- /dev/null +++ b/docs/python_api/examples/history_debug.py @@ -0,0 +1,211 @@ +# 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 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() diff --git a/docs/python_api/examples/iter_actions.py b/docs/python_api/examples/iter_actions.py new file mode 100644 index 000000000..ccc5e8b50 --- /dev/null +++ b/docs/python_api/examples/iter_actions.py @@ -0,0 +1,66 @@ +# 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) + +# annotate the function parameter so that autocomplete +# understands the type +from typing import List + + +# recursively walk the actions and their children, +# looking at marker regions. Returns a list of lines +# so we can more easily indent when recursing +def format_tree(actions: List[renderdoc.ActionDescription]): + draws, dispatches, copies = 0, 0, 0 + ret = [] + + for a in actions: + # take the flags type for brevity + ActionFlags = renderdoc.ActionFlags + + if a.flags & (ActionFlags.PushMarker | ActionFlags.MultiAction): + # markers store their name in the action's customName + # field so include that and then indent all the lines + # from recursing into the action's children + ret.append(f"{a.customName}:") + ret += [" " + l for l in format_tree(a.children)] + # 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 + + # 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:]) + + return ret + + +# the root of the recursion starts with actions at the +# root level of the capture +for line in format_tree(pyrenderdoc.CurRootActions()): + print(line) diff --git a/docs/python_api/examples/mem_binds.py b/docs/python_api/examples/mem_binds.py new file mode 100644 index 000000000..6e128097e --- /dev/null +++ b/docs/python_api/examples/mem_binds.py @@ -0,0 +1,57 @@ +# 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) + +mems = {} + +for t in pyrenderdoc.GetTextures(): + if t.memory == renderdoc.ResourceId(): + continue + + entry = ( + t.memoryOffset, + t.memoryOffset + t.byteSize, + f"[Texture] {pyrenderdoc.GetResourceName(t.resourceId)}", + ) + + if t.memory not in mems: + mems[t.memory] = [] + + mems[t.memory].append(entry) + +for b in pyrenderdoc.GetBuffers(): + if b.memory == renderdoc.ResourceId(): + continue + + entry = ( + b.memoryOffset, + b.memoryOffset + b.length, + f"[Buffer] {pyrenderdoc.GetResourceName(b.resourceId)}", + ) + + if b.memory not in mems: + mems[b.memory] = [] + + mems[b.memory].append(entry) + +for m in mems: + binds = mems[m] + + for idx, bind1 in enumerate(binds): + for bind2 in binds[idx + 1 :]: + if bind1[0] < bind2[0] and bind1[1] > bind2[0]: + print(f"In memory {pyrenderdoc.GetResourceName(m)} overlap:") + print(f" {bind1[0]:08x} - {bind1[1]:08x}: {bind1[2]} ") + print(f" {bind2[0]:08x} - {bind2[1]:08x}: {bind2[2]}") diff --git a/docs/python_api/examples/mesh_output.py b/docs/python_api/examples/mesh_output.py new file mode 100644 index 000000000..71c3a0834 --- /dev/null +++ b/docs/python_api/examples/mesh_output.py @@ -0,0 +1,153 @@ +# 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(): + raise RuntimeError("Run example with capture open and a vertex draw selected") + +import struct +from typing import List, cast + +# verify the current drawcall +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") + +controller = pyrenderdoc.GetBlockingController() + +meshdata = controller.GetPostVSData(0, 0, renderdoc.MeshDataStage.VSOut) + +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}") + +# default to just indices, but if this does use an index buffer then fetch that data +idxs = [i for i in range(meshdata.numIndices)] +if meshdata.indexResourceId != renderdoc.ResourceId(): + bufdata = controller.GetBufferData( + meshdata.indexResourceId, meshdata.indexByteOffset, meshdata.indexByteSize + ) + + # pick the appropriate format character for 1-byte, 2-byte, or 4-byte indices + # 01234 + struct_type = " BH I"[meshdata.indexByteStride] + + # use struct.unpack to interpret the bytes as a series of integers + idxs = cast( + List[int], struct.unpack(f"{struct_type}{meshdata.numIndices}", bufdata) + ) + + +def fmt_vec(vec): + return ", ".join([f"{x:.3f}" for x in vec]) + + +# print the first 4 triangles +for tri in range(min(4, meshdata.numIndices // 3)): + tri_idxs = idxs[tri * 3 : tri * 3 + 3] + + print() + print(f"Triangle {tri}:") + + for idx in tri_idxs: + print(f" [{idx}]:") + + # we expect baseVertex to be 0 - this does NOT come from the + # original draw, but we still include it + idx += meshdata.baseVertex + + offset = meshdata.vertexByteOffset + meshdata.vertexByteStride * idx + + # it would definitely be better to cache this data and look it up locally, + # but we do this to demonstrate how GetBufferData can be used + vert_data = controller.GetBufferData( + meshdata.vertexResourceId, offset, meshdata.vertexByteStride + ) + + # this could again be cached outside the per-vertex loop + + offset = 0 + + # RenderDoc always outputs the position at the beginning of the vertex + # data, so that the mesh data can be re-used for rendering without needing + # any offsets. + 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 = fmt_vec(struct.unpack_from(f"={pos.compCount}f", vert_data)) + + print(f" : {posdata}") + + offset += pos.compCount * 4 + + for output in refl.outputSignature: + # position was handled above, so skip it here + if output.systemValue == renderdoc.ShaderBuiltin.Position: + continue + + # some APIs align postvs data, to traditional 'wide' alignment: + # elements rounded up to 4 bytes and 3-wide vectors aligned to 4-wide + # in all other cases data is tightly packed + 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) + + # for simplicity we don't handle many different variable types here, only + # simple ones. You can use the varType and more complex struct formats to + # decode other types of data + 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 = "" + + name = output.varName + if name == "": + name = output.semanticIdxName + + print(f" {name}: {data}") diff --git a/docs/python_api/examples/miniqt_ui.py b/docs/python_api/examples/miniqt_ui.py new file mode 100644 index 000000000..3b968422f --- /dev/null +++ b/docs/python_api/examples/miniqt_ui.py @@ -0,0 +1,65 @@ +# 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() + +mqt = pyrenderdoc.Extensions().GetMiniQtHelper() + +# helper to make for shorter example code +def add_widget(parent, child, text=""): + mqt.AddWidget(parent, child) + if text != "": + mqt.SetWidgetText(child, text) + return child + +# create a new floating window for our example +top = mqt.CreateToplevelWidget("Example!") +pyrenderdoc.AddDockWindow(top, qrenderdoc.DockReference.NewFloatingArea, None) + +# add a group of interactive widgets +group = add_widget(top, mqt.CreateGroupBox(True), "Interactive Widgets") +layout = add_widget(group, mqt.CreateHorizontalContainer()) + +# callback for when the button is pressed that randomises the +# progress bar and counts its presses +count = 0 +def update_button(ctx=None, wid=None, text=None): + global count + mqt.SetWidgetText(butt, f"{count} button presses") + count += 1 + + import random + mqt.SetProgressBarValue(prog, random.randint(0, 100)) + +# when the checkbox is toggled, update the label +def update_checkbox(ctx=None, wid=None, text=None): + checked = "checked" if mqt.IsWidgetChecked(check) else "unchecked" + mqt.SetWidgetText(output_label, f"checkbox is {checked}") + +# create three widgets in this group +butt = add_widget(layout, mqt.CreateButton(update_button)) +check = add_widget(layout, mqt.CreateCheckbox(update_checkbox)) +output_label = add_widget(layout, mqt.CreateLabel(), "checkbox is ????") + +# create a second group of read only widgets +group = add_widget(top, mqt.CreateGroupBox(True), "Display Widgets") + +lab = add_widget(group, mqt.CreateLabel(), "A label with a funky font") +mqt.SetWidgetFont(lab, "Comic Sans MS", 15, False, True) + +prog = add_widget(group, mqt.CreateProgressBar(True)) +mqt.SetProgressBarRange(prog, 0, 100) + +readonly_text = add_widget( + group, mqt.CreateTextBox(True), "This text box is read only!" +) +mqt.SetWidgetEnabled(readonly_text, False) + +# initialise the button text to start with +update_button() diff --git a/docs/python_api/examples/pipe_state.py b/docs/python_api/examples/pipe_state.py new file mode 100644 index 000000000..74232f424 --- /dev/null +++ b/docs/python_api/examples/pipe_state.py @@ -0,0 +1,133 @@ +# 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) + +pipe = pyrenderdoc.CurPipelineState() + +get_name = lambda id: pyrenderdoc.GetResourceName(id) + +print("-------------------------") +print(" Outputs ") +print("-------------------------") + +# list all the output targets +outs = pipe.GetOutputTargets() +for i, out in enumerate(outs): + id = out.resource + # ignore any targets that are unbound + if id != renderdoc.ResourceId(): + print(f"Out {i}: {get_name(id)}") + +id = pipe.GetDepthTarget().resource +print(f"Depth: {get_name(id)}") + +print() +print("-------------------------") +print(" Pipeline/Shaders ") +print("-------------------------") + +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)}") + +print() +print("-------------------------") +print(" Constant Blocks (VS) ") +print("-------------------------") + +cbs = pipe.GetConstantBlocks(renderdoc.ShaderStage.Vertex) + +# print out the stage even though we've asked for vertex CBs. +# the index corresponds to which constant block in the vertex +# reflection was used (omitted here is the array index which +# doesn't affect that lookup) +for cb in cbs: + print( + f"{str(cb.access.stage)} CB[{cb.access.index}]: {get_name(cb.descriptor.resource)}" + ) + +print() +print("-------------------------") +print(" Descriptors by Stage ") +print("-------------------------") + +# ask for all descriptors but only those that are used +descs = pipe.GetAllUsedDescriptors(True) + +# first we'll iterate over all possible shader stages, and get all +# the descriptors for that stage +for stage in renderdoc.ShaderStage: + # silently skip stages with no used bindings + stage_descs = [d for d in descs if d.access.stage == stage] + if stage_descs == []: + continue + + # now iterate over the descriptors and print its type and the resources + # in the descriptor + 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) + + # we also print the descriptor store this is stored in + print( + f" in {get_name(d.access.descriptorStore)} at offset {d.access.byteOffset}" + ) + +print() +print("-------------------------") +print(" Descriptors by Type ") +print("-------------------------") + +# Iterate in a similar way, but this time grouping by descriptor type +for desctype in renderdoc.DescriptorType: + type_descs = [d for d in descs if d.access.type == desctype] + if type_descs == []: + continue + + print(f"** {str(desctype)} descriptors:") + for d in type_descs: + desc_str = f"{str(d.access.stage)} - " + 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}" + ) diff --git a/docs/python_api/examples/resource_usage.py b/docs/python_api/examples/resource_usage.py new file mode 100644 index 000000000..f9df46d73 --- /dev/null +++ b/docs/python_api/examples/resource_usage.py @@ -0,0 +1,54 @@ +# 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) + +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() + +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 + + 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] + + 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)}." + ) + + if len(later_usages) == 0: + print(f"{name} {pyrenderdoc.GetResourceName(id)} is never used after {eid}!") + else: + print( + f"{name} {pyrenderdoc.GetResourceName(id)} will be used as " + f"{str(later_usages[0].usage)} at {str(later_usages[0].eventId)}." + ) diff --git a/docs/python_api/examples/shader_refl.py b/docs/python_api/examples/shader_refl.py new file mode 100644 index 000000000..10b0c6f5f --- /dev/null +++ b/docs/python_api/examples/shader_refl.py @@ -0,0 +1,111 @@ +# 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) + +pyrenderdoc.CurPipelineState().GetVertexInputs() + + +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") + +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}" + ) + +print() + +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}" + ) + +print() + +print(f"VS has {len(vs.constantBlocks)} constant blocks declared") + +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}" + ) + +print() + +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}" + ) + +print() + +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}") diff --git a/docs/python_api/examples/show_buffer.py b/docs/python_api/examples/show_buffer.py new file mode 100644 index 000000000..9303fc149 --- /dev/null +++ b/docs/python_api/examples/show_buffer.py @@ -0,0 +1,57 @@ +import struct + +# 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) + +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)}") + +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 + ) + + # Get access to a controller to get the buffer data. + # We use the blocking controller for simplicity, but a better option + # might be to invoke onto the replay thread with + # pyrenderdoc.Replay().AsyncInvoke() + 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}") diff --git a/docs/python_api/examples/show_texture.py b/docs/python_api/examples/show_texture.py new file mode 100644 index 000000000..0d93ebbeb --- /dev/null +++ b/docs/python_api/examples/show_texture.py @@ -0,0 +1,76 @@ +# 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) + +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 + +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 + ) + + # Get access to a controller to get the texture saving API access. + # We use the blocking controller for simplicity, but a better option + # might be to invoke onto the replay thread with + # pyrenderdoc.Replay().AsyncInvoke() + controller = pyrenderdoc.GetBlockingController() + + filename = pyrenderdoc.Extensions().SaveFileName( + "Choose where to save JPG/PNG/DDS texture files", "", "*.jpg" + ) + + filename = filename.replace(".jpg", "") + + 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 + + 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") + + # 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") diff --git a/qrenderdoc/Resources/resources.qrc b/qrenderdoc/Resources/resources.qrc index ab37e4872..418660ed8 100644 --- a/qrenderdoc/Resources/resources.qrc +++ b/qrenderdoc/Resources/resources.qrc @@ -1,5 +1,17 @@ + ../../docs/python_api/examples/show_buffer.py + ../../docs/python_api/examples/show_texture.py + ../../docs/python_api/examples/iter_actions.py + ../../docs/python_api/examples/pipe_state.py + ../../docs/python_api/examples/shader_refl.py + ../../docs/python_api/examples/resource_usage.py + ../../docs/python_api/examples/mem_binds.py + ../../docs/python_api/examples/history_debug.py + ../../docs/python_api/examples/mesh_output.py + ../../docs/python_api/examples/miniqt_ui.py + ../../docs/python_api/examples/exe_launching.py + ../../docs/python_api/examples/event_filter.py ../../docs/stubgen.py ../Code/pyrenderdoc/parse_reflection.py diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index ddf3b7a2d..1aa6e7dda 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -325,7 +325,20 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) m_Examples->setIcon(0, Icons::help()); const QPair examples[] = { - {tr("Show a Buffer"), lit("Example will go here")}, + {tr("Tutorial: First Steps with Python"), lit(":/py/tutorial/first_steps.py")}, + {tr("Tutorial: UI extensions"), lit(":/py/tutorial/ui_extensions.py")}, + {tr("Show buffer with format"), lit(":/py/examples/show_buffer.py")}, + {tr("Show and save a texture"), lit(":/py/examples/show_texture.py")}, + {tr("Iterating over Actions"), lit(":/py/examples/iter_actions.py")}, + {tr("Pipeline State"), lit(":/py/examples/pipe_state.py")}, + {tr("Shader Reflection"), lit(":/py/examples/shader_refl.py")}, + {tr("Resource Usage"), lit(":/py/examples/resource_usage.py")}, + {tr("Memory bindings"), lit(":/py/examples/mem_binds.py")}, + {tr("Pixel History & Shader Debug"), lit(":/py/examples/history_debug.py")}, + {tr("Mesh Output"), lit(":/py/examples/mesh_output.py")}, + {tr("Launching an application"), lit(":/py/examples/exe_launching.py")}, + {tr("Custom event filter"), lit(":/py/examples/event_filter.py")}, + {tr("Mini-Qt UI"), lit(":/py/examples/miniqt_ui.py")}, }; for(const QPair &example : examples)