Files
renderdoc/util/test/tests/Iter_Test.py
T

465 lines
18 KiB
Python

from __future__ import annotations
from typing import Callable, Dict, Tuple
import rdtest
import os
import random
import renderdoc as rd
class Iter_Test(rdtest.TestCase):
slow_test = True
def save_texture(self, texsave: rd.TextureSave):
if texsave.resourceId == rd.ResourceId.Null():
return
rdtest.log.print("Saving image of " + str(texsave.resourceId))
texsave.comp.blackPoint = 0.0
texsave.comp.whitePoint = 1.0
texsave.alpha = rd.AlphaMapping.BlendToCheckerboard
filename = rdtest.get_tmp_path('texsave')
texsave.destType = rd.FileType.HDR
self.controller.SaveTexture(texsave, filename + ".hdr")
texsave.destType = rd.FileType.JPG
self.controller.SaveTexture(texsave, filename + ".jpg")
texsave.mip = -1
texsave.slice.sliceIndex = -1
texsave.destType = rd.FileType.DDS
self.controller.SaveTexture(texsave, filename + ".dds")
def image_save(self, action: rd.ActionDescription):
pipe = self.controller.GetPipelineState()
texsave = rd.TextureSave()
for res in pipe.GetOutputTargets():
texsave.resourceId = res.resource
texsave.mip = res.firstMip
self.save_texture(texsave)
depth = pipe.GetDepthTarget()
texsave.resourceId = depth.resource
texsave.mip = depth.firstMip
self.save_texture(texsave)
rdtest.log.success(f'Successfully saved images at {action.eventId}')
def compute_debug(self, action: rd.ActionDescription):
pipe = self.controller.GetPipelineState()
if pipe.GetShader(rd.ShaderStage.Compute) == rd.ResourceId.Null():
rdtest.log.print(f"No compute shader bound at {action.eventId}")
return
refl = pipe.GetShaderReflection(rd.ShaderStage.Compute)
assert refl is not None
if not (action.flags & rd.ActionFlags.Dispatch) and action.drawIndex == 0:
rdtest.log.print(f"{action.eventId} is not a debuggable action")
return
if not refl.debugInfo.debuggable:
rdtest.log.print(f"Compute shader is not debuggable at {action.eventId}")
return
wgSize = action.dispatchDimension
if any(dim == 0 for dim in wgSize):
rdtest.log.print(f"Empty dispatch ({wgSize[0]}x{wgSize[1]}x{wgSize[2]}), skipping")
return
groupid = [0,0,0]
for i in range(3):
groupid[i] = random.randint(0, wgSize[i]-1)
threadid = [0,0,0]
for i in range(3):
threadid[i] = random.randint(0, refl.dispatchThreadsDimension[i]-1)
groupid = (groupid[0], groupid[1], groupid[2])
threadid = (threadid[0], threadid[1], threadid[2])
rdtest.log.print(f"Debug Thread Workgroup:{wgSize} groupid:{groupid} threadid:{threadid}")
with self.debug_thread(groupid, threadid) as debug:
try:
cycles, variables = self.process_trace(debug.trace)
except rdtest.TestFailureException as err:
rdtest.log.error(f"Error debugging: {err.message}")
return
rdtest.log.success(f'Successfully debugged compute shader in {cycles} cycles {len(refl.outputSignature)}')
def vert_debug(self, action: rd.ActionDescription):
assert self.controller is not None
pipe = self.controller.GetPipelineState()
refl = pipe.GetShaderReflection(rd.ShaderStage.Vertex)
if pipe.GetShader(rd.ShaderStage.Vertex) == rd.ResourceId.Null():
rdtest.log.print(f"No vertex shader bound at {action.eventId}")
return
if not refl.debugInfo.debuggable:
rdtest.log.print(f"Vertex shader is not debuggable at {action.eventId}")
return
if not (action.flags & rd.ActionFlags.Drawcall) and action.drawIndex == 0:
rdtest.log.print(f"{action.eventId} is not a debuggable action")
return
vtx = int(random.random()*action.numIndices)
inst = 0
idx = vtx
if action.numIndices == 0:
rdtest.log.print("Empty action (0 vertices), skipping")
return
if action.flags & rd.ActionFlags.Instanced:
inst = int(random.random()*action.numInstances)
if action.numInstances == 0:
rdtest.log.print("Empty action (0 instances), skipping")
return
if action.flags & rd.ActionFlags.Indexed:
ib = pipe.GetIBuffer()
mesh = rd.MeshFormat()
mesh.indexResourceId = ib.resourceId
mesh.indexByteStride = ib.byteStride
mesh.indexByteOffset = ib.byteOffset + action.indexOffset * ib.byteStride
mesh.indexByteSize = ib.byteSize
mesh.baseVertex = action.baseVertex
indices = rdtest.fetch_indices(self.controller, action, mesh, 0, vtx, 1)
if len(indices) < 1:
rdtest.log.print("No index buffer, skipping")
return
idx = indices[0]
if idx is None:
rdtest.log.print("Index buffer out of bounds for idx 0, skipping")
return
striprestart_index = pipe.GetRestartIndex() & ((1 << (ib.byteStride*8)) - 1)
if pipe.IsRestartEnabled() and idx == striprestart_index:
return
rdtest.log.print("Debugging vtx %d idx %d (inst %d)" % (vtx, idx, inst))
postvs = self.get_postvs(action, rd.MeshDataStage.VSOut, first_index=vtx, num_indices=1, instance=inst)
success, err = self.check_vertex_debug(vtx, idx, inst, postvs, fatal=False, eps=5.0E-06, single_postvs=True, ignore_uninit=True)
if not success:
rdtest.log.error(f"Error debugging at EID {action.eventId}: {err}")
return
def pixel_debug(self, action: rd.ActionDescription):
pipe = self.controller.GetPipelineState()
if pipe.GetShader(rd.ShaderStage.Pixel) == rd.ResourceId.Null():
rdtest.log.print(f"No pixel shader bound at {action.eventId}")
return
if len(pipe.GetOutputTargets()) == 0 and pipe.GetDepthTarget().resource == rd.ResourceId.Null():
rdtest.log.print(f"No render targets bound at {action.eventId}")
return
if not (action.flags & rd.ActionFlags.Drawcall):
rdtest.log.print(f"{action.eventId} is not a debuggable action")
return
viewport = pipe.GetViewport(0)
# TODO, query for some pixel this action actually touched.
x = int(random.random()*viewport.width + viewport.x)
y = int(random.random()*viewport.height + viewport.y)
x = abs(x)
y = abs(y)
target = rd.ResourceId.Null()
if len(pipe.GetOutputTargets()) > 0:
valid_targets = [o.resource for o in pipe.GetOutputTargets() if o.resource != rd.ResourceId.Null()]
rdtest.log.print(f"Valid targets at {action.eventId} are {valid_targets}")
if len(valid_targets) > 0:
target = valid_targets[int(random.random()*len(valid_targets))]
if target == rd.ResourceId.Null():
target = pipe.GetDepthTarget().resource
if target == rd.ResourceId.Null():
rdtest.log.print(f"No targets bound! Can't fetch history at {action.eventId}")
return
rdtest.log.print("Fetching history for %d,%d on target %s" % (x, y, str(target)))
history = self.controller.PixelHistory(target, x, y, rd.Subresource(0, 0, 0), rd.CompType.Typeless)
rdtest.log.success("Pixel %d,%d has %d history events" % (x, y, len(history)))
lastmod = None
for i in reversed(range(len(history))):
mod = history[i]
next_action = self.find_action('', mod.eventId)
if next_action is None:
continue
action = next_action
if not(action.flags & rd.ActionFlags.Drawcall):
if action.drawIndex == 0:
continue
if not(action.flags & rd.ActionFlags.Clear):
continue
if not(action.flags & rd.ActionFlags.Copy):
continue
if not(action.flags & rd.ActionFlags.Resolve):
continue
rdtest.log.print(" hit %d at %d (%s)" % (i, mod.eventId, str(action.flags)))
lastmod = history[i]
rdtest.log.print("Got a hit on a action at event %d" % lastmod.eventId)
if mod.sampleMasked or mod.backfaceCulled or mod.depthClipped or mod.viewClipped or mod.scissorClipped or mod.shaderDiscarded or mod.depthTestFailed or mod.stencilTestFailed:
rdtest.log.print("This hit failed, looking for one that passed....")
lastmod = None
continue
if not mod.shaderOut.IsValid():
rdtest.log.print("This hit's shader out is not valid, looking for one that valid....")
lastmod = None
continue
if mod.primitiveID == 0xffffffff:
rdtest.log.print("This hit's primitive ID is invalid, looking for one that is valid....")
lastmod = None
continue
break
if target == pipe.GetDepthTarget().resource:
rdtest.log.print("Not doing pixel debug for depth output")
return
if lastmod is not None:
rdtest.log.print(f"Debugging pixel {x},{y} @ {lastmod.eventId}, primitive {lastmod.primitiveID}")
self.set_event(lastmod.eventId, True)
pipe = self.controller.GetPipelineState()
refl = pipe.GetShaderReflection(rd.ShaderStage.Pixel)
if refl is None:
rdtest.log.print(f"Nothing to debug. No pixel shader bound at {action.eventId}")
return
if not refl.debugInfo.debuggable:
rdtest.log.print(f"Pixel shader is not debuggable at {action.eventId}")
return
inputs = rd.DebugPixelInputs()
inputs.sample = 0
inputs.primitive = lastmod.primitiveID;
with self.debug_pixel(x, y, inputs) as debug:
try:
cycles, variables = self.process_trace(debug.trace)
except rdtest.TestFailureException as err:
rdtest.log.error(f"Error debugging: {err.message}")
return
output_index = [o.resource for o in pipe.GetOutputTargets()].index(target)
if action.outputs[0] == rd.ResourceId.Null():
rdtest.log.success(f'Successfully debugged pixel in {cycles} cycles, skipping result check due to no output')
elif (action.flags & rd.ActionFlags.Instanced) and action.numInstances > 1:
rdtest.log.success(f'Successfully debugged pixel in {cycles} cycles, skipping result check due to instancing')
elif pipe.GetColorBlends()[output_index].writeMask == 0:
rdtest.log.success(f'Successfully debugged pixel in {cycles} cycles, skipping result check due to write mask')
else:
rdtest.log.print(f"At event {lastmod.eventId} the target is index {output_index}")
try:
output_sourcevar = self.find_output_source_var(debug.trace, rd.ShaderBuiltin.ColorOutput, output_index)
debugged = self.evaluate_source_var(output_sourcevar, variables)
debuggedValue = list(debugged.value.f32v[0:4])
# For now, ignore debugged values that are uninitialised. This is an application bug but it causes
# false reports of problems
for idx in range(4):
if debugged.value.u32v[idx] == 0xcccccccc:
debuggedValue[idx] = lastmod.shaderOut.col.floatValue[idx]
historyValue = list(lastmod.shaderOut.col.floatValue)
tex = self.get_texture(target)
historyValue = historyValue[0:tex.format.compCount]
debuggedValue = debuggedValue[0:tex.format.compCount]
# Unfortunately we can't ever trust that we should get back a matching results, because some shaders
# rely on undefined/inaccurate maths that we don't emulate.
# So the best we can do is log an error for manual verification
is_eq, diff_amt = rdtest.value_compare_diff(historyValue, debuggedValue, eps=5.0E-06)
if not is_eq:
rdtest.log.error(
f"Debugged value {debugged.name} at EID {lastmod.eventId} {x},{y}: {diff_amt} difference. {debuggedValue} doesn't exactly match history shader output {historyValue}")
rdtest.log.success(f'Successfully debugged pixel in {cycles} cycles, result matches')
except rdtest.TestFailureException:
# This could be an application error - undefined but seen in the wild
rdtest.log.error(f"At EID {lastmod.eventId} No output variable declared for index {output_index}")
self.set_event(action.eventId, True)
def mesh_output(self, action: rd.ActionDescription):
self.controller.GetPostVSData(0, 0, rd.MeshDataStage.VSOut)
self.controller.GetPostVSData(0, 0, rd.MeshDataStage.GSOut)
rdtest.log.success('Successfully fetched mesh output')
def drawcall_overlay(self, action: rd.ActionDescription):
pipe = self.controller.GetPipelineState()
if len(pipe.GetOutputTargets()) == 0 and pipe.GetDepthTarget().resource == rd.ResourceId.Null():
rdtest.log.print(f"No render targets bound at {action.eventId}")
return
if not (action.flags & rd.ActionFlags.Drawcall):
rdtest.log.print(f"{action.eventId} is not a drawcall")
return
tex = rd.TextureDisplay()
tex.overlay = rd.DebugOverlay.Drawcall
tex.resourceId = rd.ResourceId()
col = pipe.GetOutputTargets()
depth = pipe.GetDepthTarget()
if len(col) > 1 and col[0].resource != rd.ResourceId():
tex.resourceId = col[0].resource
elif depth.resource != rd.ResourceId():
tex.resourceId = depth.resource
if tex.resourceId != rd.ResourceId():
self.texout.SetTextureDisplay(tex)
self.texout.Display()
rdtest.log.success('Successfully did drawcall overlay')
def iter_test(self):
# Handy tweaks when running locally to disable certain things
test_chance = 0.1 # Chance of doing anything at all
do_image_save = 0.25 # Chance of saving images of the outputs
do_compute_debug = 1.0 # Chance of debugging a compute thread
do_vert_debug = 1.0 # Chance of debugging a vertex (if valid)
do_pixel_debug = 1.0 # Chance of doing pixel history at the current event and debugging a pixel (if valid)
mesh_output = 1.0 # Chance of fetching mesh output data
drawcall_overlay = 0.0 # Always show drawcall overlay when we run tests
self.props = self.controller.GetAPIProperties()
event_tests: Dict[str, Tuple[float, Callable[[rd.ActionDescription], None]]] = {
'Image Save': (do_image_save, self.image_save),
'Compute Debug': (do_compute_debug, self.compute_debug),
'Vertex Debug': (do_vert_debug, self.vert_debug),
'Pixel History & Debug': (do_pixel_debug, self.pixel_debug),
'Mesh Output': (mesh_output, self.mesh_output),
'Drawcall overlay': (drawcall_overlay, self.drawcall_overlay),
}
# To choose an action, if we're going to do one, we take random in range(0, choice_max) then check each action
# type in turn to see which part of the range we landed in
choice_max = 0.0
for event_test in event_tests:
choice_max += event_tests[event_test][0]
action = self.get_first_action()
last_action = self.get_last_action()
self.texout = self.controller.CreateOutput(rd.CreateHeadlessWindowingData(100, 100), rd.ReplayOutputType.Texture)
while action:
rdtest.log.print(f"{action.eventId}/{last_action.eventId}")
self.set_event(action.eventId, False)
rdtest.log.print("Set event")
# If we should take an action at this event
if random.random() < test_chance:
c = random.random() * choice_max
for event_test in event_tests:
chance = event_tests[event_test][0]
if c < chance or chance == 0.0:
rdtest.log.print(f"Performing test '{event_test}' on event {action.eventId}")
event_tests[event_test][1](action)
break
else:
c -= chance
fatal = self.controller.GetFatalErrorStatus()
if fatal.code != rd.ResultCode.Succeeded:
rdtest.log.error(f"Fatal error detected: {fatal.Message()}")
break
action = action.nextAction
self.texout.Shutdown()
def run(self):
dir_path = self.get_ref_path('', extra=True)
for file in sorted(os.scandir(dir_path), key=lambda e: e.name.lower()):
if '.rdc' not in file.name:
continue
# Ensure we are deterministic at least from run to run by seeding with the path
random.seed(file.name)
self.filename = file.name
rdtest.log.print(f"Opening '{file.name}'.")
try:
self.controller = rdtest.open_capture(file.path)
except RuntimeError as err:
rdtest.log.print(f"Skipping. Can't open {file.path}: {err}")
continue
section_name = f'Iterating {file.name}'
if not self.validate_eventids(self.controller):
raise rdtest.TestFailureException("ERROR: capture doesn't have valid event IDs.")
rdtest.log.begin_section(section_name)
self.iter_test()
rdtest.log.end_section(section_name)
self.controller.Shutdown()
rdtest.log.success("Iterated all files")
# Useful for calling from within the UI
def run_external(self, controller: rd.ReplayController):
self.controller = controller
self.iter_test()
def run_locally(r: rd.ReplayController):
test = Iter_Test()
test.run_external(r)