From 44deaebd9086a7a3196a754863877f7e7d21c2f3 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Wed, 20 May 2026 10:38:40 +0100 Subject: [PATCH] VK_Indirect test improvements Check ALL overlays on ALL EIDs don't crash Add some IndirectCount draws to VK_Indirect for testing overlays Check overlays produce output and pixel history is not empty for each non-empty draw Check overlays produce expected output for empty draws i.e. empty except for *Pass overlay modes Check Quad Overdraw and Triangle Size overlays outputs match when comparing the Pass and Draw versions --- util/test/demos/vk/vk_indirect.cpp | 29 ++- util/test/tests/Vulkan/VK_Indirect.py | 361 ++++++++++++++++++++++---- 2 files changed, 331 insertions(+), 59 deletions(-) diff --git a/util/test/demos/vk/vk_indirect.cpp b/util/test/demos/vk/vk_indirect.cpp index e0ca870fd..7e64e6022 100644 --- a/util/test/demos/vk/vk_indirect.cpp +++ b/util/test/demos/vk/vk_indirect.cpp @@ -81,8 +81,8 @@ void main() ssbo.data[6].x = 15; // write count parameters for indirect count draws, although we might not need these - // 1 draw for non-indexed, 3 draws for indexed. - ssbo.data[10] = uvec4(1, 3, 0, 0); + // 1 draw non-indexed, 3 indexed, 3 non-indexed, 0 indexed + ssbo.data[10] = uvec4(1, 3, 3, 0); // vkCmdDrawIndirectCountKHR() ssbo.data[11] = uvec4(3, 4, 3, 4); // draw verts 3..5 @@ -94,6 +94,11 @@ void main() ssbo.data[15].x = 200; ssbo.data[16] = uvec4(6, 2, 12, 0); // draw indices 12..17 ssbo.data[17].x = 1; + + // vkCmdDrawIndirectCountKHR() + ssbo.data[20] = uvec4(3, 4, 3, 4); // draw verts 3..5 + ssbo.data[21] = uvec4(3, 2, 6, 2); // draw verts 6..8 + ssbo.data[22] = uvec4(3, 5, 9, 5); // draw verts 9..11 } } @@ -362,9 +367,23 @@ void main() // if we have KHR_draw_indirect_count, test it as well if(KHR_draw_indirect_count) { - pushMarker(cmd, "Primary: KHR_draw_indirect_count"); + pushMarker(cmd, "Primary: Indirect count draws Three & Three"); + VkViewport viewport = mainWindow->viewport; + viewport.x += viewport.width / 2.0f; + viewport.y += viewport.height / 2.0f; + viewport.width *= 0.5f; + viewport.height *= 0.5f; + vkCmdSetViewport(cmd, 0, 1, &viewport); + vkCmdDrawIndirectCountKHR(cmd, ssbo.buffer, 20 * sizeof(uvec4), ssbo.buffer, + 10 * sizeof(uvec4) + 2 * sizeof(uint32_t), 10, sizeof(uvec4)); + vkCmdDrawIndexedIndirectCountKHR(cmd, ssbo.buffer, 12 * sizeof(uvec4), ssbo.buffer, + 10 * sizeof(uvec4) + sizeof(uint32_t), 10, + sizeof(uvec4) * 2); + popMarker(cmd); + pushMarker(cmd, "Primary: KHR_action_indirect_count"); pushMarker(cmd, "Primary: Empty count draws"); + vkCmdSetViewport(cmd, 0, 1, &mainWindow->viewport); // empty draws vkCmdDrawIndirectCountKHR(cmd, ssbo.buffer, 11 * sizeof(uvec4), ssbo.buffer, 10 * sizeof(uvec4), 0, sizeof(uvec4)); @@ -372,7 +391,7 @@ void main() 10 * sizeof(uvec4) + sizeof(uint32_t), 0, sizeof(uvec4) * 2); vkCmdDrawIndexedIndirectCountKHR(cmd, ssbo.buffer, 12 * sizeof(uvec4), ssbo.buffer, - 10 * sizeof(uvec4) + sizeof(uint32_t) * 2, 10, + 10 * sizeof(uvec4) + sizeof(uint32_t) * 3, 10, sizeof(uvec4) * 2); popMarker(cmd); @@ -562,7 +581,7 @@ void main() 10 * sizeof(uvec4) + sizeof(uint32_t), 0, sizeof(uvec4) * 2); vkCmdDrawIndexedIndirectCountKHR(cmd, ssbo.buffer, 12 * sizeof(uvec4), ssbo.buffer, - 10 * sizeof(uvec4) + sizeof(uint32_t) * 2, 10, + 10 * sizeof(uvec4) + sizeof(uint32_t) * 3, 10, sizeof(uvec4) * 2); popMarker(cmd); diff --git a/util/test/tests/Vulkan/VK_Indirect.py b/util/test/tests/Vulkan/VK_Indirect.py index ea808e8fb..fc5394d64 100644 --- a/util/test/tests/Vulkan/VK_Indirect.py +++ b/util/test/tests/Vulkan/VK_Indirect.py @@ -1,7 +1,7 @@ import rdtest import struct import renderdoc as rd - +from typing import List def real_action_children(action): return [c for c in action.children if not c.flags & rd.ActionFlags.PopMarker] @@ -10,6 +10,66 @@ def real_action_children(action): class VK_Indirect(rdtest.TestCase): demos_test_name = 'VK_Indirect' + samples = [ + (50, 40), + (60, 40), + (70, 40), + + (90, 40), + (100, 40), + (110, 40), + + (130, 40), + (140, 40), + (160, 40), + + (190, 40), + (200, 40), + (220, 40), + + + + (50, 190), + (60, 190), + (70, 190), + + (90, 190), + (100, 190), + (110, 190), + + (130, 190), + (140, 190), + (160, 190), + + (190, 190), + (200, 190), + (220, 190), + + + + (330, 40), + (340, 40), + (350, 40), + + (330, 115), + (340, 115), + (350, 115), + + (330, 190), + (340, 190), + (350, 190), + ] + + def check_pixel_history_succeeds(self, eid: int, x: int, y: int): + pipe: rd.PipeState = self.controller.GetPipelineState() + rt = pipe.GetOutputTargets()[0] + tex = rt.resource + sub = rd.Subresource() + modifs: List[rd.PixelModification] = self.controller.PixelHistory(tex, x, y, sub, rt.format.compType) + if len(modifs) < 2: + raise rdtest.TestFailureException(f"EID: {eid} No pixel history found at ({x}, {y})") + rdtest.log.success(f"EID: {eid} Pixel History {x}, {y} Worked") + def check_overlay(self, pass_samples, *, no_overlay = False): pipe: rd.PipeState = self.controller.GetPipelineState() @@ -23,70 +83,238 @@ class VK_Indirect(rdtest.TestCase): overlay_id = self.out.GetDebugOverlayTexID() - samples = [ - (50, 40), - (60, 40), - (70, 40), - - (90, 40), - (100, 40), - (110, 40), - - (130, 40), - (140, 40), - (160, 40), - - (190, 40), - (200, 40), - (220, 40), - - - - (50, 190), - (60, 190), - (70, 190), - - (90, 190), - (100, 190), - (110, 190), - - (130, 190), - (140, 190), - (160, 190), - - (190, 190), - (200, 190), - (220, 190), - - - - (330, 40), - (340, 40), - (350, 40), - - (330, 115), - (340, 115), - (350, 115), - - (330, 190), - (340, 190), - (350, 190), - ] - # Every sample that isn't passing should be off off_alpha = 0.5 # If the overlay isn't even for a action, it will be cleared to black if no_overlay: - off_alpha = 0.0 self.check(len(pass_samples) == 0) - for s in [s for s in samples if s not in pass_samples]: + for s in [s for s in self.samples if s not in pass_samples]: self.check_pixel_value(overlay_id, s[0], s[1], [0.0, 0.0, 0.0, off_alpha], eps=1.0/256.0) # And the passing samples should be on for s in pass_samples: self.check_pixel_value(overlay_id, s[0], s[1], [0.8, 0.1, 0.8, 1.0], eps=1.0/256.0) + def get_overlay_pixel(self, overlay: rd.DebugOverlay, col_tex: rd.ResourceId, x: int, y: int): + tex = rd.TextureDisplay() + tex.resourceId = col_tex + tex.overlay = overlay + tex.subresource.sample = 0 + + out: rd.ReplayOutput = self.controller.CreateOutput(rd.CreateHeadlessWindowingData(100, 100), rd.ReplayOutputType.Texture) + out.SetTextureDisplay(tex) + out.Display() + overlayTex: rd.ResourceId = out.GetDebugOverlayTexID() + if overlay == rd.DebugOverlay.ClearBeforeDraw: + overlayTex = col_tex + if overlay == rd.DebugOverlay.ClearBeforePass: + overlayTex = col_tex + + picked = self.controller.PickPixel(overlayTex, x, y, rd.Subresource(), rd.CompType.UNorm) + out.Shutdown() + return picked + + def check_overlays(self, eid: int, x: int, y: int): + with rdtest.log.auto_section(f'EID {eid} Checking Overlays at {x}, {y}'): + pipe: rd.PipeState = self.controller.GetPipelineState() + if len(pipe.GetOutputTargets()) == 0: + raise rdtest.TestFailureException("No output targets found") + + col_tex: rd.ResourceId = pipe.GetOutputTargets()[0].resource + + for overlay in rd.DebugOverlay: + if overlay == rd.DebugOverlay.NoOverlay: + continue + if overlay == rd.DebugOverlay.NaN or overlay == rd.DebugOverlay.Clipping: + continue + if overlay == rd.DebugOverlay.ViewportScissor: + continue + + emptyPixel = (0.0, 0.0, 0.0, 0.0) + picked = self.get_overlay_pixel(overlay, col_tex, x, y) + if picked.floatValue == emptyPixel: + raise rdtest.TestFailureException(f"{overlay.name} overlay is empty") + + # Check "Quad Overdraw (Draw)/(Pass)" match + pickedDraw = self.get_overlay_pixel(rd.DebugOverlay.QuadOverdrawDraw, col_tex, x, y) + pickedPass = self.get_overlay_pixel(rd.DebugOverlay.QuadOverdrawPass, col_tex, x, y) + if pickedDraw.floatValue != pickedPass.floatValue: + raise rdtest.TestFailureException(f"Quad Overdraw Draw and Pass do not match: {pickedDraw.floatValue} vs {pickedPass.floatValue}") + pickedDraw = self.get_overlay_pixel(rd.DebugOverlay.TriangleSizeDraw, col_tex, x, y) + pickedPass = self.get_overlay_pixel(rd.DebugOverlay.TriangleSizePass, col_tex, x, y) + if pickedDraw.floatValue != pickedPass.floatValue: + raise rdtest.TestFailureException(f"Triangle Size Draw and Pass do not match: {pickedDraw.floatValue} vs {pickedPass.floatValue}") + + def check_overlay_and_pixel_history(self, eid, coords): + self.controller.SetFrameEvent(eid, False) + for c in coords: + x = c[0] + y = c[1] + self.check_overlays(eid, x, y) + self.check_pixel_history_succeeds(eid, x, y) + + def check_overlays_and_pixel_history(self): + for level in ["Primary", "Secondary"]: + action = self.find_action(f"{level}: Indirect draws") + + # vkCmdDrawIndirect : 1 + action = self.find_action("vkCmdDrawIndirect", action.eventId) + eid = action.eventId + coords = [(60, 60)] + self.check_overlay_and_pixel_history(eid, coords) + + # vkCmdDrawIndexedIndirect : 2 + action = self.find_action("vkCmdDrawIndexedIndirect", action.eventId) + eid = action.eventId + # Draw Action 1 + eid += 1 + coords = [(100, 60)] + self.check_overlay_and_pixel_history(eid, coords) + # Draw Action 2 + eid += 1 + coords = [(140, 40), (200, 40)] + self.check_overlay_and_pixel_history(eid, coords) + + action = self.find_action(f"{level}: Indirect count draws Three & Three") + if action: + # vkCmdDrawIndirectCount : 3 + action = self.find_action("vkCmdDrawIndirectCount", action.eventId) + eid = action.eventId + # Draw Action 1 + eid += 1 + coords = [(230, 250)] + self.check_overlay_and_pixel_history(eid, coords) + # Draw Action 2 + eid += 1 + coords = [(250, 180)] + self.check_overlay_and_pixel_history(eid, coords) + # Draw Action 3 + eid += 1 + coords = [(270, 170)] + self.check_overlay_and_pixel_history(eid, coords) + + # vkCmdDrawIndexedIndirectCount : 3 + action = self.find_action("vkCmdDrawIndexedIndirectCount", action.eventId) + eid = action.eventId + # Draw Action 1 + eid += 1 + coords = [(250, 250)] + self.check_overlay_and_pixel_history(eid, coords) + # Draw Action 2 : no draw + eid += 1 + # Draw Action 3 + eid += 1 + coords = [(270, 245),(300,245)] + self.check_overlay_and_pixel_history(eid, coords) + + action = self.find_action(f"{level}: KHR_action_indirect_count") + if action: + # Primary: Indirect count draws + action = self.find_action(f"{level}: Indirect count draws", action.eventId) + # vkCmdDrawIndirectCount : 1 + action = self.find_action("vkCmdDrawIndirectCount", action.eventId) + eid = action.eventId + # Draw Action 1 + eid += 1 + coords = [(60, 200)] + self.check_overlay_and_pixel_history(eid, coords) + # vkCmdDrawIndexedIndirectCount : 3 + action = self.find_action("vkCmdDrawIndexedIndirectCount", action.eventId) + eid = action.eventId + # Draw Action 1 + eid += 1 + coords = [(100, 200)] + self.check_overlay_and_pixel_history(eid, coords) + # Draw Action 2 : no draw + eid += 1 + # Draw Action 3 + eid += 1 + coords = [(140, 190),(200,190)] + self.check_overlay_and_pixel_history(eid, coords) + + # Primary : Post-count 1 + action = self.find_action(f"{level}: Post-count 1") + # vkCmdDraw : 1 + action = self.find_action("vkCmdDraw", action.eventId) + eid = action.eventId + coords = [(340, 60)] + self.check_overlay_and_pixel_history(eid, coords) + + # Primary : Post-count 2 + action = self.find_action(f"{level}: Post-count 2") + # vkCmdDraw : 1 + action = self.find_action("vkCmdDraw", action.eventId) + eid = action.eventId + coords = [(340, 200)] + self.check_overlay_and_pixel_history(eid, coords) + + # Primary : Post-count 3 + action = self.find_action(f"{level}: Post-count 3") + # vkCmdDraw : 1 + action = self.find_action("vkCmdDraw", action.eventId) + eid = action.eventId + coords = [(340, 140)] + self.check_overlay_and_pixel_history(eid, coords) + + def check_empty_draw_overlays(self): + with rdtest.log.auto_section('Checking Empty Draws'): + for level in ["Primary", "Secondary"]: + empties = self.find_action(f"{level}: Empty count draws") + action: rd.ActionDescription + for action in real_action_children(empties): + eid = action.eventId + self.controller.SetFrameEvent(eid, False) + pipe = self.controller.GetPipelineState() + for overlay in rd.DebugOverlay: + if overlay == rd.DebugOverlay.NoOverlay: + continue + if overlay == rd.DebugOverlay.NaN or overlay == rd.DebugOverlay.Clipping: + continue + if overlay == rd.DebugOverlay.ViewportScissor: + continue + if overlay == rd.DebugOverlay.Wireframe: + continue + tex = rd.TextureDisplay() + col_tex: rd.ResourceId = pipe.GetOutputTargets()[0].resource + tex.resourceId = col_tex + tex.overlay = overlay + tex.subresource.sample = 0 + out: rd.ReplayOutput = self.controller.CreateOutput(rd.CreateHeadlessWindowingData(100, 100), rd.ReplayOutputType.Texture) + out.SetTextureDisplay(tex) + out.Display() + overlayTex: rd.ResourceId = out.GetDebugOverlayTexID() + expectEmpty = True + if overlay == rd.DebugOverlay.ClearBeforeDraw: + overlayTex = col_tex + if overlay == rd.DebugOverlay.ClearBeforePass: + overlayTex = col_tex + if overlay == rd.DebugOverlay.ClearBeforePass: + expectEmpty = False + if overlay == rd.DebugOverlay.QuadOverdrawPass: + expectEmpty = False + if overlay == rd.DebugOverlay.TriangleSizePass: + expectEmpty = False + + empty = True + emptyPixel = (0.0, 0.0, 0.0, 0.0) + if overlay == rd.DebugOverlay.Drawcall: + emptyPixel = (0.0, 0.0, 0.0, 0.5) + + for s in self.samples: + x = s[0] + y = s[1] + picked = self.controller.PickPixel(overlayTex, x, y, rd.Subresource(), rd.CompType.Float) + if picked.floatValue != emptyPixel: + empty = False + if expectEmpty and not empty: + raise rdtest.TestFailureException(f"EID {eid} {overlay.name} {x}, {y} {picked.floatValue} is not as expected {emptyPixel}") + if expectEmpty != empty: + raise rdtest.TestFailureException(f"EID {eid} {overlay.name} is not as expected") + + out.Shutdown() + def check_capture(self): + fill = self.find_action("vkCmdFillBuffer") self.check(fill is not None) @@ -286,6 +514,7 @@ class VK_Indirect(rdtest.TestCase): rdtest.log.success("{} {} is as expected".format(level, action.customName)) if indirect_count_root is not None: + rdtest.log.print(f"{indirect_count_root.children[1].customName}") self.check(indirect_count_root.children[0].customName == '{}: Empty count draws'.format(level)) self.check(indirect_count_root.children[1].customName == '{}: Indirect count draws'.format(level)) @@ -294,7 +523,7 @@ class VK_Indirect(rdtest.TestCase): self.check(empties and len(real_action_children(empties)) == 3) action: rd.ActionDescription - for action in real_action_children(empties.children): + for action in real_action_children(empties): self.check(action.numIndices == 0) self.check(action.numInstances == 0) @@ -422,3 +651,27 @@ class VK_Indirect(rdtest.TestCase): self.check_overlay([(340, 115)]) else: rdtest.log.print("KHR_action_indirect_count not tested") + + with rdtest.log.auto_section('Checking Overlays And Pixel History'): + self.check_overlays_and_pixel_history() + + with rdtest.log.auto_section('Checking All Overlays'): + for eid in range(self.get_first_action().eventId, self.get_last_action().eventId + 1): + self.controller.SetFrameEvent(eid, False) + pipe = self.controller.GetPipelineState() + if len(pipe.GetOutputTargets()) == 0: + continue + rdtest.log.print(f"EID: {eid}") + for overlay in rd.DebugOverlay: + tex = rd.TextureDisplay() + col_tex: rd.ResourceId = pipe.GetOutputTargets()[0].resource + tex.resourceId = col_tex + tex.overlay = overlay + tex.subresource.sample = 0 + + out: rd.ReplayOutput = self.controller.CreateOutput(rd.CreateHeadlessWindowingData(100, 100), rd.ReplayOutputType.Texture) + out.SetTextureDisplay(tex) + out.Display() + out.Shutdown() + + self.check_empty_draw_overlays() \ No newline at end of file