From fa0ba4a532bc2d3313e4f87ee122929985f5336a Mon Sep 17 00:00:00 2001 From: baldurk Date: Mon, 18 Aug 2025 12:41:39 +0100 Subject: [PATCH] Don't disable blending for fetching per-fragment postmod --- renderdoc/driver/d3d12/d3d12_pixelhistory.cpp | 4 +- util/test/demos/d3d12/d3d12_blend.cpp | 174 ++++++++++++++++++ util/test/demos/demos.vcxproj | 1 + util/test/demos/demos.vcxproj.filters | 3 + .../tests/D3D12/D3D12_Blend_Pixel_History.py | 157 ++++++++++++++++ 5 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 util/test/demos/d3d12/d3d12_blend.cpp create mode 100644 util/test/tests/D3D12/D3D12_Blend_Pixel_History.py diff --git a/renderdoc/driver/d3d12/d3d12_pixelhistory.cpp b/renderdoc/driver/d3d12/d3d12_pixelhistory.cpp index a66bc9296..d2c931f10 100644 --- a/renderdoc/driver/d3d12/d3d12_pixelhistory.cpp +++ b/renderdoc/driver/d3d12/d3d12_pixelhistory.cpp @@ -2312,9 +2312,11 @@ struct D3D12PixelHistoryPerFragmentCallback : D3D12PixelHistoryCallback // Mask out writes to targets which aren't the pixel history color target for(uint32_t i = 0; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) { - pipeDesc.BlendState.RenderTarget[i].BlendEnable = FALSE; if(i != colorOutputIndex) + { + pipeDesc.BlendState.RenderTarget[i].BlendEnable = FALSE; pipeDesc.BlendState.RenderTarget[i].RenderTargetWriteMask = 0; + } } } diff --git a/util/test/demos/d3d12/d3d12_blend.cpp b/util/test/demos/d3d12/d3d12_blend.cpp new file mode 100644 index 000000000..3858fa9a3 --- /dev/null +++ b/util/test/demos/d3d12/d3d12_blend.cpp @@ -0,0 +1,174 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019-2025 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "d3d12_test.h" + +RD_TEST(D3D12_Blend, D3D12GraphicsTest) +{ + static constexpr const char *Description = + "Draws a triangle repeatedly to test blending within a single drawcall"; + + const DefaultA2V TemplateTriangleRed[3] = { + {Vec3f(-0.5f, -0.5f, 0.0f), Vec4f(1 / 255.f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(0.0f, 0.5f, 0.0f), Vec4f(1 / 255.f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(0.5f, -0.5f, 0.0f), Vec4f(1 / 255.f, 0.0f, 0.0f, 1.0f), Vec2f(1.0f, 0.0f)}, + }; + const int TRIANGLES_RED_INDEX = 0; + const int NUM_TRIANGLES_RED = 16; + const DefaultA2V TemplateTriangleGreen[3] = { + {Vec3f(-0.5f, -0.5f, 0.0f), Vec4f(0.0f, 1 / 255.f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(0.0f, 0.5f, 0.0f), Vec4f(0.0f, 1 / 255.f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(0.5f, -0.5f, 0.0f), Vec4f(0.0f, 1 / 255.f, 0.0f, 1.0f), Vec2f(1.0f, 0.0f)}, + }; + const int TRIANGLES_GREEN_INDEX = TRIANGLES_RED_INDEX + NUM_TRIANGLES_RED; + const int NUM_TRIANGLES_GREEN = 255; + const DefaultA2V TemplateTriangleBlue[3] = { + {Vec3f(-0.5f, -0.5f, 0.0f), Vec4f(0.0f, 0.0f, 1 / 255.f, 1.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(0.0f, 0.5f, 0.0f), Vec4f(0.0f, 0.0f, 1 / 255.f, 1.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(0.5f, -0.5f, 0.0f), Vec4f(0.0f, 0.0f, 1 / 255.f, 1.0f), Vec2f(1.0f, 0.0f)}, + }; + const int TRIANGLES_BLUE_INDEX = TRIANGLES_GREEN_INDEX + NUM_TRIANGLES_GREEN; + const int NUM_TRIANGLES_BLUE = 512; + + const int NUM_TRIANGLES_TOTAL = TRIANGLES_BLUE_INDEX + NUM_TRIANGLES_BLUE; + + int main() + { + // initialise, create window, create device, etc + if(!Init()) + return 3; + + std::vector triangles; + triangles.reserve(3 * NUM_TRIANGLES_TOTAL); + for(int i = 0; i < NUM_TRIANGLES_RED; i++) + { + triangles.push_back(TemplateTriangleRed[0]); + triangles.push_back(TemplateTriangleRed[1]); + triangles.push_back(TemplateTriangleRed[2]); + } + for(int i = 0; i < NUM_TRIANGLES_GREEN; i++) + { + triangles.push_back(TemplateTriangleGreen[0]); + triangles.push_back(TemplateTriangleGreen[1]); + triangles.push_back(TemplateTriangleGreen[2]); + } + for(int i = 0; i < NUM_TRIANGLES_BLUE; i++) + { + triangles.push_back(TemplateTriangleBlue[0]); + triangles.push_back(TemplateTriangleBlue[1]); + triangles.push_back(TemplateTriangleBlue[2]); + } + + ID3D12ResourcePtr vb = MakeBuffer().Data(triangles); + + ID3D12ResourcePtr img = MakeTexture(DXGI_FORMAT_R32G32B32A32_FLOAT, screenWidth, screenHeight) + .RTV() + .InitialState(D3D12_RESOURCE_STATE_RENDER_TARGET); + D3D12_CPU_DESCRIPTOR_HANDLE rtv = MakeRTV(img).CreateCPU(5); + + ID3DBlobPtr vsblob = Compile(D3DDefaultVertex, "main", "vs_4_0"); + ID3DBlobPtr psblob = Compile(D3DDefaultPixel, "main", "ps_4_0"); + + ID3D12RootSignaturePtr sig = MakeSig({}); + + D3D12PSOCreator psoInfo = + MakePSO().RootSig(sig).InputLayout().RTVs({DXGI_FORMAT_R32G32B32A32_FLOAT}).VS(vsblob).PS(psblob); + + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].BlendEnable = TRUE; + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ONE; + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE; + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO; + psoInfo.GraphicsDesc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; + + ID3D12PipelineStatePtr pso = psoInfo; + + while(Running()) + { + ID3D12GraphicsCommandListPtr cmd = GetCommandBuffer(); + + Reset(cmd); + + ID3D12ResourcePtr bb = StartUsingBackbuffer(cmd, D3D12_RESOURCE_STATE_RENDER_TARGET); + + ClearRenderTargetView(cmd, BBRTV, {0.2f, 0.2f, 0.2f, 1.0f}); + + pushMarker(cmd, "Clear"); + OMSetRenderTargets(cmd, {rtv}, {}); + ClearRenderTargetView(cmd, rtv, {0.0f, 0.0f, 0.0f, 1.0f}); + popMarker(cmd); + + cmd->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + IASetVertexBuffer(cmd, vb, sizeof(DefaultA2V), 0); + cmd->SetPipelineState(pso); + cmd->SetGraphicsRootSignature(sig); + + SetMainWindowViewScissor(cmd); + + pushMarker(cmd, "Red: groups of repeated draws"); + for(int i = 1; i <= NUM_TRIANGLES_RED; i *= 2) + { + cmd->DrawInstanced(3 * i, 1, TRIANGLES_RED_INDEX, 0); + } + setMarker(cmd, "End of red"); + popMarker(cmd); + pushMarker(cmd, "Green: 255 (the maximum we can handle) in a single drawcall"); + cmd->DrawInstanced(3 * NUM_TRIANGLES_GREEN, 1, 3 * TRIANGLES_GREEN_INDEX, 0); + popMarker(cmd); + pushMarker(cmd, "Blue: 512 (more than the maximum) in a single drawcall"); + cmd->DrawInstanced(3 * NUM_TRIANGLES_BLUE, 1, 3 * TRIANGLES_BLUE_INDEX, 0); + popMarker(cmd); + + pushMarker(cmd, "Clear"); + ClearRenderTargetView(cmd, rtv, {0.0f, 0.0f, 0.0f, 1.0f}); + popMarker(cmd); + + pushMarker(cmd, "All of the above in a single drawcall"); + cmd->DrawInstanced(3 * NUM_TRIANGLES_TOTAL, 1, 3 * TRIANGLES_RED_INDEX, 0); + popMarker(cmd); + + setMarker(cmd, "Test End"); + + ResourceBarrier(cmd, img, D3D12_RESOURCE_STATE_RENDER_TARGET, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + blitToSwap(cmd, img, bb, DXGI_FORMAT_R32G32B32A32_FLOAT); + + ResourceBarrier(cmd, img, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_RENDER_TARGET); + + FinishUsingBackbuffer(cmd, D3D12_RESOURCE_STATE_RENDER_TARGET); + + cmd->Close(); + + SubmitAndPresent({cmd}); + } + + return 0; + } +}; + +REGISTER_TEST(); diff --git a/util/test/demos/demos.vcxproj b/util/test/demos/demos.vcxproj index 3606a9cd8..570ad26d7 100644 --- a/util/test/demos/demos.vcxproj +++ b/util/test/demos/demos.vcxproj @@ -192,6 +192,7 @@ + diff --git a/util/test/demos/demos.vcxproj.filters b/util/test/demos/demos.vcxproj.filters index 0fc18faca..923972402 100644 --- a/util/test/demos/demos.vcxproj.filters +++ b/util/test/demos/demos.vcxproj.filters @@ -733,6 +733,9 @@ Vulkan\demos + + D3D12\demos + diff --git a/util/test/tests/D3D12/D3D12_Blend_Pixel_History.py b/util/test/tests/D3D12/D3D12_Blend_Pixel_History.py new file mode 100644 index 000000000..35a47925a --- /dev/null +++ b/util/test/tests/D3D12/D3D12_Blend_Pixel_History.py @@ -0,0 +1,157 @@ +import renderdoc as rd +import rdtest +from typing import List + +def value_selector(x): return x.floatValue +def passed(x): return x.Passed() +def event_id(x): return x.eventId +def culled(x): return x.backfaceCulled +def depth_test_failed(x): return x.depthTestFailed +def depth_clipped(x): return x.depthClipped +def depth_bounds_failed(x): return x.depthBoundsFailed +def scissor_clipped(x): return x.scissorClipped +def stencil_test_failed(x): return x.stencilTestFailed +def shader_discarded(x): return x.shaderDiscarded +def shader_out_col(x): return value_selector(x.shaderOut.col) +def shader_out_depth(x): return x.shaderOut.depth +def pre_mod_col(x): return value_selector(x.preMod.col) +def post_mod_col(x): return value_selector(x.postMod.col) +def shader_out_depth(x): return x.shaderOut.depth +def pre_mod_depth(x): return x.preMod.depth +def post_mod_depth(x): return x.postMod.depth +def primitive_id(x): return x.primitiveID +def unboundPS(x): return x.unboundPS + +NUM_TRIANGLES_RED = 16 +NUM_TRIANGLES_RED_REAL = NUM_TRIANGLES_RED * 2 - 1 +NUM_TRIANGLES_GREEN = 255 +NUM_TRIANGLES_BLUE = 512 + +class D3D12_Blend_Pixel_History(rdtest.TestCase): + demos_test_name = 'D3D12_Blend' + + def check_capture(self): + apiprops: rd.APIProperties = self.controller.GetAPIProperties() + + self.primary_test() + + def primary_test(self): + test_marker: rd.ActionDescription = self.find_action("Test End") + self.controller.SetFrameEvent(test_marker.eventId, True) + + pipe: rd.PipeState = self.controller.GetPipelineState() + + rt = pipe.GetOutputTargets()[0] + + tex = rt.resource + tex_details = self.get_texture(tex) + + sub = rd.Subresource() + if tex_details.arraysize > 1: + sub.slice = rt.firstSlice + if tex_details.mips > 1: + sub.mip = rt.firstMip + + red_eid = self.find_action("Red: ").next.eventId + red_last_eid = self.find_action("End of red").next.eventId + green_eid = self.find_action("Green: ").next.eventId + blue_eid = self.find_action("Blue: ").next.eventId + all_eid = self.find_action("All of the above in a single drawcall").next.eventId + + # Pixel inside of all of the triangles + x, y = 200, 150 + rdtest.log.print("Testing pixel {}, {}".format(x, y)) + modifs: List[rd.PixelModification] = self.controller.PixelHistory(tex, x, y, sub, rt.format.compType) + self.check_modifs_consistent(modifs) + red_modifs = [m for m in modifs if m.eventId >= red_eid and m.eventId < red_last_eid] + green_modifs = [m for m in modifs if m.eventId == green_eid] + blue_modifs = [m for m in modifs if m.eventId == blue_eid] + all_modifs = [m for m in modifs if m.eventId == all_eid] + + if len(red_modifs) != NUM_TRIANGLES_RED_REAL: + raise rdtest.TestFailureException("Expected {} modifications for red triangles (EIDS {} until {}) but got {}".format(NUM_TRIANGLES_RED_REAL, red_eid, red_last_eid, len(red_modifs))) + + for i, modif in enumerate(red_modifs): + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for red triangle {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for red triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0))) + + i = 1 + eid_counter = 0 + modif_counter = 0 + while i <= NUM_TRIANGLES_RED: + for primitive_id in range(i): + if red_modifs[modif_counter].eventId != red_eid + eid_counter: + raise rdtest.TestFailureException("Expected red triangle {} to be part of EID {} but was {}".format(modif_counter, red_eid + eid_counter, red_modifs[modif_counter].eventId)) + if red_modifs[modif_counter].primitiveID != primitive_id: + raise rdtest.TestFailureException("Expected red triangle {} to have primitive ID {} but was {}".format(modif_counter, primitive_id, red_modifs[modif_counter].primitiveID)) + modif_counter += 1 + eid_counter += 1 + i *= 2 + + if len(green_modifs) != NUM_TRIANGLES_GREEN: + raise rdtest.TestFailureException("Expected {} modifications for green triangles (EID {}) but got {}".format(NUM_TRIANGLES_GREEN, green_eid, len(gren_modifs))) + + for i, modif in enumerate(green_modifs): + if modif.primitiveID != i: + raise rdtest.TestFailureException("Expected green triangle {} to have primitive ID {} but was {}".format(i, primitive_id, modif.primitiveID)) + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for green triangle {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, (i+1)/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for green triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, (i+1)/255.0, 0.0, 1.0))) + + # We can only record 255 modifications due to the stencil format + if len(blue_modifs) != 255: + raise rdtest.TestFailureException("Expected {} modifications for blue triangles (EID {}) but got {}".format(255, blue_eid, len(blue_modifs))) + + for i, modif in enumerate(blue_modifs): + if modif.primitiveID != i: + raise rdtest.TestFailureException("Expected blue triangle {} to have primitive ID {} but was {}".format(i, primitive_id, modif.primitiveID)) + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (0.0, 0.0, 1.0/255.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for blue triangle {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (0.0, 0.0, 1.0/255.0, 1.0))) + if i == 254: + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, 1.0, NUM_TRIANGLES_BLUE/255.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for final blue triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, 1.0, NUM_TRIANGLES_BLUE/255.0, 1.0))) + else: + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, 1.0, (i+1)/255.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for blue triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, 1.0, (i+1)/255.0, 1.0))) + + # Once again, we can only record 255 modifications due to the stencil format + if len(all_modifs) != 255: + raise rdtest.TestFailureException("Expected {} modifications for all triangles (EID {}) but got {}".format(255, all_eid, len(all_modifs))) + + for i, modif in enumerate(all_modifs): + if modif.primitiveID != i: + raise rdtest.TestFailureException("Expected triangle {} in all to have primitive ID {} but was {}".format(i, primitive_id, modif.primitiveID)) + + if i < NUM_TRIANGLES_RED: + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for red triangle in all {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for red triangle in all {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0))) + else: + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for green triangle in all {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0))) + if i != 254: + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, (i+1-NUM_TRIANGLES_RED)/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for green triangle in all {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, (i+1-NUM_TRIANGLES_RED)/255.0, 0.0, 1.0))) + else: + # For i = 254 (the last triangle), the post-mod value is always set to the final post-mod value, but everything else is correctly set to the 255th modification + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, 1.0, NUM_TRIANGLES_BLUE/255.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for final (blue) triangle in all {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, 1.0, NUM_TRIANGLES_BLUE/255.0, 1.0))) + + def check_modifs_consistent(self, modifs): + # postmod of each should match premod of the next + for i in range(len(modifs) - 1): + a = value_selector(modifs[i].postMod.col) + b = value_selector(modifs[i + 1].preMod.col) + + if a != b: + raise rdtest.TestFailureException( + "postmod at {} primitive {}: {} doesn't match premod at {} primitive {}: {}".format(modifs[i].eventId, + modifs[i].primitiveID, + a, + modifs[i + 1].eventId, + modifs[i + 1].primitiveID, + b))