rdc_dump.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env python3
  2. """RenderDoc capture inspector for this engine's GL puppet-compositing pipeline.
  3. Runs inside qrenderdoc's embedded Python interpreter (not a normal python3 - the
  4. `renderdoc`/`qrenderdoc` modules only exist there). Invoke as:
  5. env -u WAYLAND_DISPLAY QT_QPA_PLATFORM=xcb DISPLAY=:0 \
  6. RDC_CAPFILE=/path/to/capture.rdc RDC_MODE=positions RDC_EVENTID=1775 \
  7. qrenderdoc --python tools/rdc_dump.py
  8. Modes (set via RDC_MODE):
  9. list - print eventId/name/numIndices/outputs for every draw call in the capture.
  10. positions - dump the a_Position vertex buffer for RDC_EVENTID (comma-separated list ok).
  11. texcoords - dump the a_TexCoord vertex buffer for RDC_EVENTID.
  12. uniforms - dump the $Globals uniform block (g_ModelViewProjectionMatrix etc) for RDC_EVENTID.
  13. resources - dump the fragment shader's bound texture resourceIds/names for RDC_EVENTID,
  14. useful for confirming two draws are part of the same object's FBO->composite chain
  15. (an intermediate pass's output resourceId showing up as a later draw's input).
  16. screenshot - save the final swapchain image of the whole frame (last draw call) to RDC_OUT (PNG).
  17. Several embedded-API quirks aren't documented anywhere obvious - see the per-function comments
  18. below (GetConstantBlock's resourceId lives under .descriptor.resource not .resourceId, resource
  19. names need a manual resourceId->name dict from controller.GetResources() since there's no
  20. GetResourceName(), etc).
  21. """
  22. import os
  23. import sys
  24. import struct
  25. import renderdoc as rd
  26. CAPFILE = os.environ.get("RDC_CAPFILE")
  27. MODE = os.environ.get("RDC_MODE", "list")
  28. EVENTIDS = [int(e) for e in os.environ.get("RDC_EVENTID", "").split(",") if e.strip()]
  29. OUT = os.environ.get("RDC_OUT")
  30. out_lines = []
  31. def log(s):
  32. print(s)
  33. out_lines.append(str(s))
  34. def walk_actions(controller, fn):
  35. def recurse(act):
  36. fn(act)
  37. for c in act.children:
  38. recurse(c)
  39. for a in controller.GetRootActions():
  40. recurse(a)
  41. def dump_vbuffer(controller, eid, attr_name):
  42. controller.SetFrameEvent(eid, True)
  43. state = controller.GetPipelineState()
  44. vp = state.GetViewport(0)
  45. vbs = state.GetVBuffers()
  46. attrs = state.GetVertexInputs()
  47. log(f"eventId={eid} viewport=({vp.x},{vp.y},{vp.width},{vp.height})")
  48. for attr in attrs:
  49. if attr.name != attr_name:
  50. continue
  51. vb = vbs[attr.vertexBuffer]
  52. total_offset = attr.byteOffset + vb.byteOffset
  53. data = controller.GetBufferData(vb.resourceId, total_offset, 0)
  54. comp = attr.format.compCount
  55. n = (len(data) // 4 // comp) * comp
  56. vals = struct.unpack_from(f"<{n}f", data, 0)
  57. log(f" {attr_name} ({comp} floats/vertex, {n // comp} vertices): {vals}")
  58. if comp >= 2:
  59. xs = vals[0::comp]
  60. ys = vals[1::comp]
  61. log(f" {attr_name} X range: {min(xs)} to {max(xs)} (center {(min(xs)+max(xs))/2})")
  62. log(f" {attr_name} Y range: {min(ys)} to {max(ys)} (center {(min(ys)+max(ys))/2})")
  63. def dump_uniforms(controller, eid):
  64. controller.SetFrameEvent(eid, True)
  65. state = controller.GetPipelineState()
  66. refl = state.GetShaderReflection(rd.ShaderStage.Vertex)
  67. vs_id = state.GetShader(rd.ShaderStage.Vertex)
  68. # GetConstantBlock's return type (BoundCBuffer) hides the actual resourceId/byteOffset/byteSize
  69. # under .descriptor, not as direct attributes - not obvious from the method name.
  70. cbuf = state.GetConstantBlock(rd.ShaderStage.Vertex, 0, 0)
  71. d = cbuf.descriptor
  72. variables = controller.GetCBufferVariableContents(
  73. state.GetGraphicsPipelineObject(), vs_id, rd.ShaderStage.Vertex, refl.entryPoint, 0,
  74. d.resource, d.byteOffset, d.byteSize)
  75. log(f"eventId={eid} $Globals:")
  76. for v in variables:
  77. log(f" {v.name}: {v.value.f32v[:16]}")
  78. def dump_resources(controller, eid, resnames):
  79. controller.SetFrameEvent(eid, True)
  80. state = controller.GetPipelineState()
  81. def act_info():
  82. found = [(None, None)]
  83. def fn(act):
  84. if act.eventId == eid:
  85. found[0] = (act.GetName(controller.GetStructuredFile()), list(act.outputs))
  86. walk_actions(controller, fn)
  87. return found[0]
  88. name, outputs = act_info()
  89. log(f"eventId={eid} name={name} outputs={outputs}")
  90. for ro in state.GetReadOnlyResources(rd.ShaderStage.Fragment):
  91. rid = ro.descriptor.resource
  92. log(f" fragment slot={ro.access.index} resourceId={rid} name={resnames.get(rid, '?')}")
  93. def do_list(controller):
  94. def fn(act):
  95. if act.flags & rd.ActionFlags.Drawcall:
  96. log(f"eventId={act.eventId} name={act.GetName(controller.GetStructuredFile())} "
  97. f"numIndices={act.numIndices} outputs={list(act.outputs)}")
  98. walk_actions(controller, fn)
  99. def do_screenshot(controller):
  100. last_draw = [None]
  101. def fn(act):
  102. if act.flags & rd.ActionFlags.Drawcall:
  103. last_draw[0] = act
  104. walk_actions(controller, fn)
  105. if last_draw[0] is None:
  106. log("no draw calls found")
  107. return
  108. controller.SetFrameEvent(last_draw[0].eventId, True)
  109. log(f"last draw eventId={last_draw[0].eventId}")
  110. texsave = rd.TextureSave()
  111. # the output resourceId lives on the *action*, not the controller - there's no
  112. # controller.GetOutputTargets() despite PipeState having one for the currently bound FBO.
  113. texsave.resourceId = last_draw[0].outputs[0]
  114. if texsave.resourceId == rd.ResourceId.Null():
  115. log("no output resource on last draw")
  116. return
  117. texsave.destType = rd.FileType.PNG
  118. texsave.mip = 0
  119. texsave.slice.sliceIndex = 0
  120. ok = controller.SaveTexture(texsave, OUT or "screenshot.png")
  121. log(f"SaveTexture -> {OUT or 'screenshot.png'} ok={ok}")
  122. def sampleCode(controller):
  123. if MODE == "list":
  124. do_list(controller)
  125. elif MODE == "positions":
  126. for eid in EVENTIDS:
  127. dump_vbuffer(controller, eid, "a_Position")
  128. elif MODE == "texcoords":
  129. for eid in EVENTIDS:
  130. dump_vbuffer(controller, eid, "a_TexCoord")
  131. elif MODE == "uniforms":
  132. for eid in EVENTIDS:
  133. dump_uniforms(controller, eid)
  134. elif MODE == "resources":
  135. resnames = {r.resourceId: r.name for r in controller.GetResources()}
  136. for eid in EVENTIDS:
  137. dump_resources(controller, eid, resnames)
  138. elif MODE == "screenshot":
  139. do_screenshot(controller)
  140. else:
  141. log(f"unknown RDC_MODE={MODE!r}")
  142. def main():
  143. if not CAPFILE:
  144. log("RDC_CAPFILE env var not set")
  145. return
  146. import time
  147. pyrenderdoc.LoadCapture(CAPFILE, rd.ReplayOptions(), CAPFILE, False, True)
  148. for _ in range(200):
  149. if pyrenderdoc.IsCaptureLoaded():
  150. break
  151. time.sleep(0.1)
  152. log(f"IsCaptureLoaded: {pyrenderdoc.IsCaptureLoaded()}")
  153. pyrenderdoc.Replay().BlockInvoke(sampleCode)
  154. try:
  155. main()
  156. except Exception as e:
  157. import traceback
  158. log(f"EXCEPTION: {e}")
  159. log(traceback.format_exc())
  160. log_path = OUT + ".log" if (OUT and MODE == "screenshot") else OUT
  161. if log_path:
  162. with open(log_path, "w") as f:
  163. f.write("\n".join(out_lines))
  164. os._exit(0)