| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- #!/usr/bin/env python3
- """RenderDoc capture inspector for this engine's GL puppet-compositing pipeline.
- Runs inside qrenderdoc's embedded Python interpreter (not a normal python3 - the
- `renderdoc`/`qrenderdoc` modules only exist there). Invoke as:
- env -u WAYLAND_DISPLAY QT_QPA_PLATFORM=xcb DISPLAY=:0 \
- RDC_CAPFILE=/path/to/capture.rdc RDC_MODE=positions RDC_EVENTID=1775 \
- qrenderdoc --python tools/rdc_dump.py
- Modes (set via RDC_MODE):
- list - print eventId/name/numIndices/outputs for every draw call in the capture.
- positions - dump the a_Position vertex buffer for RDC_EVENTID (comma-separated list ok).
- texcoords - dump the a_TexCoord vertex buffer for RDC_EVENTID.
- uniforms - dump the $Globals uniform block (g_ModelViewProjectionMatrix etc) for RDC_EVENTID.
- resources - dump the fragment shader's bound texture resourceIds/names for RDC_EVENTID,
- useful for confirming two draws are part of the same object's FBO->composite chain
- (an intermediate pass's output resourceId showing up as a later draw's input).
- screenshot - save the final swapchain image of the whole frame (last draw call) to RDC_OUT (PNG).
- Several embedded-API quirks aren't documented anywhere obvious - see the per-function comments
- below (GetConstantBlock's resourceId lives under .descriptor.resource not .resourceId, resource
- names need a manual resourceId->name dict from controller.GetResources() since there's no
- GetResourceName(), etc).
- """
- import os
- import sys
- import struct
- import renderdoc as rd
- CAPFILE = os.environ.get("RDC_CAPFILE")
- MODE = os.environ.get("RDC_MODE", "list")
- EVENTIDS = [int(e) for e in os.environ.get("RDC_EVENTID", "").split(",") if e.strip()]
- OUT = os.environ.get("RDC_OUT")
- out_lines = []
- def log(s):
- print(s)
- out_lines.append(str(s))
- def walk_actions(controller, fn):
- def recurse(act):
- fn(act)
- for c in act.children:
- recurse(c)
- for a in controller.GetRootActions():
- recurse(a)
- def dump_vbuffer(controller, eid, attr_name):
- controller.SetFrameEvent(eid, True)
- state = controller.GetPipelineState()
- vp = state.GetViewport(0)
- vbs = state.GetVBuffers()
- attrs = state.GetVertexInputs()
- log(f"eventId={eid} viewport=({vp.x},{vp.y},{vp.width},{vp.height})")
- for attr in attrs:
- if attr.name != attr_name:
- continue
- vb = vbs[attr.vertexBuffer]
- total_offset = attr.byteOffset + vb.byteOffset
- data = controller.GetBufferData(vb.resourceId, total_offset, 0)
- comp = attr.format.compCount
- n = (len(data) // 4 // comp) * comp
- vals = struct.unpack_from(f"<{n}f", data, 0)
- log(f" {attr_name} ({comp} floats/vertex, {n // comp} vertices): {vals}")
- if comp >= 2:
- xs = vals[0::comp]
- ys = vals[1::comp]
- log(f" {attr_name} X range: {min(xs)} to {max(xs)} (center {(min(xs)+max(xs))/2})")
- log(f" {attr_name} Y range: {min(ys)} to {max(ys)} (center {(min(ys)+max(ys))/2})")
- def dump_uniforms(controller, eid):
- controller.SetFrameEvent(eid, True)
- state = controller.GetPipelineState()
- refl = state.GetShaderReflection(rd.ShaderStage.Vertex)
- vs_id = state.GetShader(rd.ShaderStage.Vertex)
- # GetConstantBlock's return type (BoundCBuffer) hides the actual resourceId/byteOffset/byteSize
- # under .descriptor, not as direct attributes - not obvious from the method name.
- cbuf = state.GetConstantBlock(rd.ShaderStage.Vertex, 0, 0)
- d = cbuf.descriptor
- variables = controller.GetCBufferVariableContents(
- state.GetGraphicsPipelineObject(), vs_id, rd.ShaderStage.Vertex, refl.entryPoint, 0,
- d.resource, d.byteOffset, d.byteSize)
- log(f"eventId={eid} $Globals:")
- for v in variables:
- log(f" {v.name}: {v.value.f32v[:16]}")
- def dump_resources(controller, eid, resnames):
- controller.SetFrameEvent(eid, True)
- state = controller.GetPipelineState()
- def act_info():
- found = [(None, None)]
- def fn(act):
- if act.eventId == eid:
- found[0] = (act.GetName(controller.GetStructuredFile()), list(act.outputs))
- walk_actions(controller, fn)
- return found[0]
- name, outputs = act_info()
- log(f"eventId={eid} name={name} outputs={outputs}")
- for ro in state.GetReadOnlyResources(rd.ShaderStage.Fragment):
- rid = ro.descriptor.resource
- log(f" fragment slot={ro.access.index} resourceId={rid} name={resnames.get(rid, '?')}")
- def do_list(controller):
- def fn(act):
- if act.flags & rd.ActionFlags.Drawcall:
- log(f"eventId={act.eventId} name={act.GetName(controller.GetStructuredFile())} "
- f"numIndices={act.numIndices} outputs={list(act.outputs)}")
- walk_actions(controller, fn)
- def do_screenshot(controller):
- last_draw = [None]
- def fn(act):
- if act.flags & rd.ActionFlags.Drawcall:
- last_draw[0] = act
- walk_actions(controller, fn)
- if last_draw[0] is None:
- log("no draw calls found")
- return
- controller.SetFrameEvent(last_draw[0].eventId, True)
- log(f"last draw eventId={last_draw[0].eventId}")
- texsave = rd.TextureSave()
- # the output resourceId lives on the *action*, not the controller - there's no
- # controller.GetOutputTargets() despite PipeState having one for the currently bound FBO.
- texsave.resourceId = last_draw[0].outputs[0]
- if texsave.resourceId == rd.ResourceId.Null():
- log("no output resource on last draw")
- return
- texsave.destType = rd.FileType.PNG
- texsave.mip = 0
- texsave.slice.sliceIndex = 0
- ok = controller.SaveTexture(texsave, OUT or "screenshot.png")
- log(f"SaveTexture -> {OUT or 'screenshot.png'} ok={ok}")
- def sampleCode(controller):
- if MODE == "list":
- do_list(controller)
- elif MODE == "positions":
- for eid in EVENTIDS:
- dump_vbuffer(controller, eid, "a_Position")
- elif MODE == "texcoords":
- for eid in EVENTIDS:
- dump_vbuffer(controller, eid, "a_TexCoord")
- elif MODE == "uniforms":
- for eid in EVENTIDS:
- dump_uniforms(controller, eid)
- elif MODE == "resources":
- resnames = {r.resourceId: r.name for r in controller.GetResources()}
- for eid in EVENTIDS:
- dump_resources(controller, eid, resnames)
- elif MODE == "screenshot":
- do_screenshot(controller)
- else:
- log(f"unknown RDC_MODE={MODE!r}")
- def main():
- if not CAPFILE:
- log("RDC_CAPFILE env var not set")
- return
- import time
- pyrenderdoc.LoadCapture(CAPFILE, rd.ReplayOptions(), CAPFILE, False, True)
- for _ in range(200):
- if pyrenderdoc.IsCaptureLoaded():
- break
- time.sleep(0.1)
- log(f"IsCaptureLoaded: {pyrenderdoc.IsCaptureLoaded()}")
- pyrenderdoc.Replay().BlockInvoke(sampleCode)
- try:
- main()
- except Exception as e:
- import traceback
- log(f"EXCEPTION: {e}")
- log(traceback.format_exc())
- log_path = OUT + ".log" if (OUT and MODE == "screenshot") else OUT
- if log_path:
- with open(log_path, "w") as f:
- f.write("\n".join(out_lines))
- os._exit(0)
|