From 5238ee0c4a079c3530587a1540760d470393bcf2 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 15:05:14 -0700 Subject: [PATCH 01/39] [PIX] Add validation support to PIX pass tests The PIX passes change DXIL after the compiler completes. A pass can add a resource, change a root signature, insert an operation, and remove a declaration. The result can be a module that the validator refuses. The tests only examine the disassembly, so they cannot find this type of defect. The new helpers run the DXIL validator on the output of a pass. They also separate the diagnostics that PIX instrumentation is permitted to cause from the diagnostics that are defects. The set of permitted diagnostics decides what every later layer can ignore, so it is the part to examine with care. There is no change to the compiler. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- tools/clang/unittests/HLSL/PixTest.cpp | 247 +++++++++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 18c8a1c58b..e21546c9ac 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -160,6 +160,12 @@ class PixTest : public ::testing::Test { TEST_METHOD(NonUniformResourceIndex_DescriptorHeap) TEST_METHOD(NonUniformResourceIndex_Raytracing) + // Control tests for the PIX pass validation harness below + // (ValidateInstrumentedModule / VerifyInstrumentedModuleIsValid). + TEST_METHOD(Validation_ControlValidModulePasses) + TEST_METHOD(Validation_ControlInvalidModuleFails) + TEST_METHOD(Validation_ControlBoilerplateOnlyFailureIsRejected) + dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -263,6 +269,142 @@ class PixTest : public ::testing::Test { std::move(pOptimizedModule), {}, Tokenize(outputText.c_str(), "\n")}; } + // Runs one named PIX or DXIL pass and returns the resulting module and + // its disassembly lines. + struct SinglePassOutput { + CComPtr Module; + std::vector Lines; + }; + + SinglePassOutput RunSinglePass(IDxcBlob *dxil, LPCWSTR passOption) { + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-opt-mod-passes"); + Options.push_back(passOption); + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + SinglePassOutput ret; + ret.Module = pOptimizedModule; + ret.Lines = Tokenize(BlobToUtf8(pText).c_str(), "\n"); + return ret; + } + + // PIX does not validate the shaders its passes instrument, so a pass + // that produces invalid DXIL goes undetected elsewhere. Validate here + // instead. + struct ValidationResult { + bool Valid; + std::string Errors; + }; + + ValidationResult ValidateInstrumentedModule(IDxcBlob *pModule) { + CComPtr pContainer; + + // Some pass runners return a bare bitcode module; others already + // return a container. The validator accepts only a container. + if (hlsl::IsDxilContainerLike(pModule->GetBufferPointer(), + pModule->GetBufferSize()) != nullptr) { + pContainer = pModule; + } else { + pContainer = pix_test::WrapInNewContainer(m_dllSupport, pModule); + } + + CComPtr pValidator; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcValidator, &pValidator)); + + CComPtr pValidationResult; + VERIFY_SUCCEEDED(pValidator->Validate(pContainer, DxcValidatorFlags_Default, + &pValidationResult)); + + HRESULT validationStatus; + VERIFY_SUCCEEDED(pValidationResult->GetStatus(&validationStatus)); + if (SUCCEEDED(validationStatus)) { + return {true, {}}; + } + + CComPtr pValidationErrors; + VERIFY_SUCCEEDED(pValidationResult->GetErrorBuffer(&pValidationErrors)); + return {false, BlobToUtf8(pValidationErrors)}; + } + + // Significant holds validator diagnostics other than boilerplate and the + // permitted metadata exception. PermittedExceptionCount counts the + // exception separately. + struct FilteredValidationDiagnostics { + std::vector Significant; + int PermittedExceptionCount = 0; + }; + + // Filters out boilerplate ("Validation failed.") and the permitted + // metadata exception: virtual-register annotation passes add metadata + // that DXIL does not consume, so the validator reports it as unused. Do + // not widen this filter. + FilteredValidationDiagnostics + GetSignificantValidationDiagnostics(const std::string &errors) { + FilteredValidationDiagnostics result; + std::stringstream errorStream(errors); + std::string line; + while (std::getline(errorStream, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (line.empty() || line == "Validation failed.") { + continue; + } + if (line.find("All metadata must be used by dxil") != std::string::npos) { + result.PermittedExceptionCount++; + continue; + } + result.Significant.push_back(line); + } + return result; + } + + // True only if the diagnostics contain no significant errors and at + // least one instance of the permitted metadata exception. + bool IsPermittedValidationException( + const FilteredValidationDiagnostics &diagnostics) { + return diagnostics.Significant.empty() && + diagnostics.PermittedExceptionCount > 0; + } + + // Asserts an instrumented module validates. Accepts a module whose only + // diagnostic is the permitted metadata exception; logs and fails on any + // other validator error. + void VerifyInstrumentedModuleIsValid(IDxcBlob *pModule, + const char *description) { + ValidationResult validation = ValidateInstrumentedModule(pModule); + if (validation.Valid) { + return; + } + + FilteredValidationDiagnostics diagnostics = + GetSignificantValidationDiagnostics(validation.Errors); + if (IsPermittedValidationException(diagnostics)) { + return; + } + + std::string joined; + if (diagnostics.Significant.empty()) { + joined = "(validator reported failure with no significant diagnostic " + "text, and no permitted metadata exception was found)"; + } else { + for (auto const &significantError : diagnostics.Significant) { + joined += significantError + "\n"; + } + } + WEX::Logging::Log::Error(WEX::Common::String().Format( + L"Validation failed after %S:\n%S", description, joined.c_str())); + VERIFY_FAIL(); + } + CComPtr FindModule(hlsl::DxilFourCC fourCC, IDxcBlob *pSource) { const UINT32 BC_C0DE = ((INT32)(INT8)'B' | (INT32)(INT8)'C' << 8 | (INT32)0xDEC0 << 16); // BC0xc0de in big endian @@ -3467,3 +3609,108 @@ void main(uint3 tid : SV_DispatchThreadID) { } VERIFY_ARE_EQUAL(debugBreakBitSetCount, 2); } + +/////////////////////////////////////////////////////////////////////////////// +// Control tests for the PIX pass validation harness +// (ValidateInstrumentedModule / VerifyInstrumentedModuleIsValid). +// +// Both tests instrument the same trivial pixel shader with the +// virtual-register annotation pass, so the valid and invalid cases are +// directly comparable. + +TEST_F(PixTest, Validation_ControlValidModulePasses) { + const char *source = R"x( +float main() : SV_Target +{ + return 0; +})x"; + + // Virtual-register annotation adds metadata that DXIL does not consume, + // so this module only validates via the permitted metadata exception. + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + VerifyInstrumentedModuleIsValid( + output.Module, + "virtual-register annotation of a trivial pixel shader (validation " + "harness control)"); +} + +TEST_F(PixTest, Validation_ControlInvalidModuleFails) { + const char *source = R"x( +float main() : SV_Target +{ + return 0; +})x"; + + // Same shader and pass as Validation_ControlValidModulePasses; only the + // corruption below differs. + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + + // Confirm the baseline validates before corrupting it, so the failure + // below is caused by the corruption and nothing else. + VerifyInstrumentedModuleIsValid( + output.Module, + "virtual-register annotation of a trivial pixel shader, uncorrupted " + "baseline (validation harness control)"); + + // Mislabel the shader stage. The validator must reject this regardless + // of the permitted metadata exception. + std::string disassembly = Disassemble(output.Module); + const std::string shaderKindTag = "!\"ps\","; + auto tagPosition = disassembly.find(shaderKindTag); + VERIFY_IS_TRUE(tagPosition != std::string::npos); + disassembly.replace(tagPosition, shaderKindTag.size(), "!\"vs\","); + + CComPtr pDisassemblyBlob; + CreateBlobFromText(m_dllSupport, disassembly.c_str(), &pDisassemblyBlob); + + CComPtr pAssembler; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcAssembler, &pAssembler)); + CComPtr pAssembleResult; + VERIFY_SUCCEEDED( + pAssembler->AssembleToContainer(pDisassemblyBlob, &pAssembleResult)); + HRESULT assembleStatus; + VERIFY_SUCCEEDED(pAssembleResult->GetStatus(&assembleStatus)); + VERIFY_SUCCEEDED(assembleStatus); + CComPtr pCorruptedContainer; + VERIFY_SUCCEEDED(pAssembleResult->GetResult(&pCorruptedContainer)); + + ValidationResult validation = ValidateInstrumentedModule(pCorruptedContainer); + VERIFY_IS_FALSE(validation.Valid); + + // Confirm the corruption produces a real diagnostic, not just the + // permitted metadata exception. + FilteredValidationDiagnostics diagnostics = + GetSignificantValidationDiagnostics(validation.Errors); + VERIFY_IS_FALSE(diagnostics.Significant.empty()); +} + +// Tests that a validator failure is rejected unless its only diagnostic is +// the permitted metadata exception. A failure with only the "Validation +// failed." boilerplate and no exception must not pass. +TEST_F(PixTest, Validation_ControlBoilerplateOnlyFailureIsRejected) { + // Boilerplate only, no permitted exception: must be rejected. + FilteredValidationDiagnostics boilerplateOnly = + GetSignificantValidationDiagnostics("Validation failed.\n"); + VERIFY_IS_TRUE(boilerplateOnly.Significant.empty()); + VERIFY_ARE_EQUAL(boilerplateOnly.PermittedExceptionCount, 0); + VERIFY_IS_FALSE(IsPermittedValidationException(boilerplateOnly)); + + // Permitted exception present: must be accepted. + FilteredValidationDiagnostics exceptionOnly = + GetSignificantValidationDiagnostics( + "Validation failed.\n" + "All metadata must be used by dxil's users.\n"); + VERIFY_IS_TRUE(exceptionOnly.Significant.empty()); + VERIFY_IS_TRUE(exceptionOnly.PermittedExceptionCount > 0); + VERIFY_IS_TRUE(IsPermittedValidationException(exceptionOnly)); + + // Real diagnostic present: must be rejected, even with the exception. + FilteredValidationDiagnostics realDiagnostic = + GetSignificantValidationDiagnostics("Validation failed.\n" + "Some real validator diagnostic.\n"); + VERIFY_IS_FALSE(realDiagnostic.Significant.empty()); + VERIFY_IS_FALSE(IsPermittedValidationException(realDiagnostic)); +} From 0e1a37420e2111a2b4fb72808707075cb5121dfc Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 19:58:17 -0700 Subject: [PATCH 02/39] [PIX] Fix shared instrumentation resource handling More than one PIX pass can run on the same module. Each pass adds its own tools UAV, so a pipeline with two passes makes two resources at the same register and space. The code that adds this UAV to a root signature has three unsafe paths. Root signature serialization can fail and give a null blob, which the code then reads. An empty result can replace a correct root signature. Only the first global root-signature subobject gets the new parameter, so a state object that declares more than one is left in a mixed state. The tools UAV is a raw buffer, so it changes the shader flags of the module. The code does not compute the flags again. Some passes make a dx.op overload declaration and then find no callers for it. They leave the declaration in the module, and the validator refuses a module that has an unused declaration. When serialization fails, the helpers keep the original root signature. A valid signature is better than an empty one. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilDebugBreakInstrumentation.cpp | 6 +- .../DxilDebugInstrumentation.cpp | 17 - ...NonUniformResourceIndexInstrumentation.cpp | 3 + .../DxilOutputColorBecomesConstant.cpp | 5 + ...lPIXAddTidToAmplificationShaderPayload.cpp | 3 + ...DxilPIXMeshShaderOutputInstrumentation.cpp | 9 + lib/DxilPIXPasses/DxilRemoveDiscards.cpp | 4 + lib/DxilPIXPasses/PixPassHelpers.cpp | 115 ++-- lib/DxilPIXPasses/PixPassHelpers.h | 2 +- .../HLSLFileCheck/pix/pixelCounterEarlyZ.hlsl | 6 +- tools/clang/unittests/HLSL/PixTest.cpp | 595 +++++++++++++++++- 11 files changed, 684 insertions(+), 81 deletions(-) diff --git a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp index 30799c4bdc..dc27f4b469 100644 --- a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp @@ -120,10 +120,8 @@ bool DxilDebugBreakInstrumentation::runOnModule(Module &M) { CI->eraseFromParent(); } - // Clean up the now-unused declaration. Not strictly required for - // correctness, but keeps the module free of dead references. - if (DebugBreakFunc->use_empty()) - DebugBreakFunc->eraseFromParent(); + PIXPassHelpers::EraseIfUnused(DM, DebugBreakFunc); + PIXPassHelpers::EraseIfUnused(DM, AtomicOpFunc); const bool modified = (PixUAVResource != nullptr); diff --git a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp index 4dd43b07cc..a40acfe860 100644 --- a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp @@ -1436,23 +1436,6 @@ bool DxilDebugInstrumentation::RunOnFunction(Module &M, DxilModule &DM, auto &values = m_FunctionToValues[BC.Builder.GetInsertBlock()->getParent()]; - // PIX binds two UAVs when running this instrumentation: one for raygen - // shaders and another for the hitgroups and miss shaders. Since PIX invokes - // this pass at the library level, which may contain examples of both types, - // PIX can't really specify which UAV index to use per-shader. This pass - // therefore just has to know this: - constexpr unsigned int RayGenUAVRegister = 0; - constexpr unsigned int HitGroupAndMissUAVRegister = 1; - unsigned int UAVRegisterId = RayGenUAVRegister; - switch (shaderKind) { - case DXIL::ShaderKind::ClosestHit: - case DXIL::ShaderKind::Intersection: - case DXIL::ShaderKind::AnyHit: - case DXIL::ShaderKind::Miss: - UAVRegisterId = HitGroupAndMissUAVRegister; - break; - } - values.UAVHandle = PIXPassHelpers::CreateHandleForResource( DM, Builder, uav, "PIX_DebugUAV_Handle"); diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index a442bfabed..3c7a542648 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -149,6 +149,9 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { const bool modified = (PixUAVResource != nullptr); + PIXPassHelpers::EraseIfUnused(DM, WaveActiveAllEqualFunc); + PIXPassHelpers::EraseIfUnused(DM, AtomicOpFunc); + if (modified) { DM.ReEmitDxilResources(); diff --git a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp index a0749fbe0f..227cc890a7 100644 --- a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp +++ b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp @@ -125,6 +125,8 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { [&hasIntOutputs](CallInst *) { hasIntOutputs = true; }); if (!hasFloatOutputs && !hasIntOutputs) { + PIXPassHelpers::EraseIfUnused(DM, FloatOutputFunction); + PIXPassHelpers::EraseIfUnused(DM, IntOutputFunction); return false; } @@ -251,6 +253,9 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { }); } + PIXPassHelpers::EraseIfUnused(DM, FloatOutputFunction); + PIXPassHelpers::EraseIfUnused(DM, IntOutputFunction); + return Modified; } diff --git a/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp b/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp index e756e98781..c459d4e768 100644 --- a/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp +++ b/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp @@ -183,8 +183,11 @@ bool DxilPIXAddTidToAmplificationShaderPayload::runOnModule(Module &M) { {DispatchMeshOpcode, DispatchMesh.get_threadGroupCountX(), DispatchMesh.get_threadGroupCountY(), DispatchMesh.get_threadGroupCountZ(), NewStructAlloca}); + llvm::Function *OriginalDispatchMeshFn = + cast(&*I)->getCalledFunction(); I->removeFromParent(); delete &*I; + PIXPassHelpers::EraseIfUnused(DM, OriginalDispatchMeshFn); // Validation requires exactly one DispatchMesh in an AS, so we can exit // after the first one: DM.ReEmitDxilResources(); diff --git a/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp b/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp index 507a718a4c..418c285576 100644 --- a/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp @@ -310,6 +310,8 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { } if (getMeshPayloadInstructions != nullptr) { + llvm::Function *OriginalGetMeshPayloadFunction = + cast(getMeshPayloadInstructions)->getCalledFunction(); Function *DxilFunc = HlslOP->GetOpFunc( OP::OpCode::GetMeshPayload, expanded.ExpandedPayloadStructPtrType); @@ -326,6 +328,7 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { ReplaceAllUsesOfInstructionWithNewValueAndDeleteInstruction( getMeshPayloadInstructions, payload, expanded.ExpandedPayloadStructType); + PIXPassHelpers::EraseIfUnused(DM, OriginalGetMeshPayloadFunction); } } @@ -378,9 +381,11 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { {Type::getInt16Ty(Ctx), int16ValueIndicator}, {Type::getFloatTy(Ctx), floatValueIndicator}, {Type::getHalfTy(Ctx), float16ValueIndicator}}; + SmallVector StoreVertexOutputFunctions; for (auto const &Overload : StoreVertexOutputOverloads) { F = HlslOP->GetOpFunc(DXIL::OpCode::StoreVertexOutput, Overload.type); + StoreVertexOutputFunctions.push_back(F); FunctionUses = F->uses(); for (auto FI = FunctionUses.begin(); FI != FunctionUses.end();) { auto &FunctionUse = *FI++; @@ -419,6 +424,10 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { } } + for (Function *StoreVertexOutputFunction : StoreVertexOutputFunctions) { + PIXPassHelpers::EraseIfUnused(DM, StoreVertexOutputFunction); + } + DM.ReEmitDxilResources(); return true; diff --git a/lib/DxilPIXPasses/DxilRemoveDiscards.cpp b/lib/DxilPIXPasses/DxilRemoveDiscards.cpp index 5f8d9263f0..72b2bf88cd 100644 --- a/lib/DxilPIXPasses/DxilRemoveDiscards.cpp +++ b/lib/DxilPIXPasses/DxilRemoveDiscards.cpp @@ -17,6 +17,8 @@ #include "llvm/IR/Instructions.h" #include "llvm/IR/PassManager.h" +#include "PixPassHelpers.h" + using namespace llvm; using namespace hlsl; @@ -53,6 +55,8 @@ bool DxilRemoveDiscards::runOnModule(Module &M) { Modified = true; } + PIXPassHelpers::EraseIfUnused(DM, DiscardFunction); + return Modified; } diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index c7c99cf763..1137fe6cf8 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -185,6 +185,9 @@ static std::vector SerializeRootSignatureToVector( SerializeRootSignature(rootSignature, &serializedRootSignature, &errorBlob, allowReservedRegisterSpace); std::vector ret; + if (serializedRootSignature == nullptr) { + return ret; + } auto const *serializedData = reinterpret_cast( serializedRootSignature->GetBufferPointer()); ret.assign(serializedData, @@ -194,10 +197,9 @@ static std::vector SerializeRootSignatureToVector( } constexpr uint32_t toolsRegisterSpace = static_cast(-2); -constexpr uint32_t toolsUAVRegister = 0; template -void ExtendRootSig(RootSigDesc &rootSigDesc) { +void ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { auto *existingParams = rootSigDesc.pParameters; for (uint32_t i = 0; i < rootSigDesc.NumParameters; ++i) { if (rootSigDesc.pParameters[i].ParameterType == @@ -229,17 +231,20 @@ void ExtendRootSig(RootSigDesc &rootSigDesc) { rootSigDesc.NumParameters++; } -static std::vector AddUAVParamterToRootSignature(const void *Data, - uint32_t Size) { +static std::vector +AddUAVParamterToRootSignature(const void *Data, uint32_t Size, + uint32_t toolsUAVRegister) { DxilVersionedRootSignature rootSignature; DeserializeRootSignature(Data, Size, rootSignature.get_address_of()); auto *rs = rootSignature.get_mutable(); switch (rootSignature->Version) { case DxilRootSignatureVersion::Version_1_0: - ExtendRootSig(rs->Desc_1_0); + ExtendRootSig(rs->Desc_1_0, + toolsUAVRegister); break; case DxilRootSignatureVersion::Version_1_1: - ExtendRootSig(rs->Desc_1_1); + ExtendRootSig(rs->Desc_1_1, + toolsUAVRegister); rs->Desc_1_1.pParameters[rs->Desc_1_1.NumParameters - 1].Descriptor.Flags = hlsl::DxilRootDescriptorFlags::None; break; @@ -247,16 +252,26 @@ static std::vector AddUAVParamterToRootSignature(const void *Data, return SerializeRootSignatureToVector(rs); } -static void AddUAVToShaderAttributeRootSignature(DxilModule &DM) { +static void AddUAVToShaderAttributeRootSignature(DxilModule &DM, + uint32_t toolsUAVRegister) { auto rs = DM.GetSerializedRootSignature(); if (!rs.empty()) { std::vector asVector = AddUAVParamterToRootSignature( - rs.data(), static_cast(rs.size())); - DM.ResetSerializedRootSignature(asVector); + rs.data(), static_cast(rs.size()), toolsUAVRegister); + if (!asVector.empty()) { + DM.ResetSerializedRootSignature(asVector); + } } } -static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM) { +static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, + uint32_t toolsUAVRegister) { + struct ReplacementRootSignature { + std::string Name; + std::vector Data; + }; + + std::vector replacementRootSignatures; auto *subObjects = DM.GetSubobjects(); if (subObjects != nullptr) { for (auto const &subObject : subObjects->GetSubobjects()) { @@ -267,16 +282,24 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM) { constexpr bool notALocalRS = false; if (subObject.second->GetRootSignature(notALocalRS, Data, Size, nullptr)) { - auto extendedRootSig = AddUAVParamterToRootSignature(Data, Size); - auto rootSignatureSubObjectName = subObject.first; - subObjects->RemoveSubobject(rootSignatureSubObjectName); - subObjects->CreateRootSignature( - rootSignatureSubObjectName, notALocalRS, extendedRootSig.data(), - static_cast(extendedRootSig.size())); - break; + std::vector extended = + AddUAVParamterToRootSignature(Data, Size, toolsUAVRegister); + if (!extended.empty()) { + replacementRootSignatures.push_back( + {subObject.first.str(), std::move(extended)}); + } } } } + + constexpr bool notALocalRS = false; + for (auto const &replacementRootSignature : replacementRootSignatures) { + subObjects->RemoveSubobject(replacementRootSignature.Name); + subObjects->CreateRootSignature( + replacementRootSignature.Name, notALocalRS, + replacementRootSignature.Data.data(), + static_cast(replacementRootSignature.Data.size())); + } } } @@ -286,6 +309,13 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, const char *name) { LLVMContext &Ctx = DM.GetModule()->getContext(); + for (auto const &existingUAV : DM.GetUAVs()) { + if (existingUAV->GetSpaceID() == toolsRegisterSpace && + existingUAV->GetLowerBound() == hlslBindIndex) { + return existingUAV.get(); + } + } + const char *PIXStructTypeName = ShaderModelHandleTypeName(DM); llvm::StructType *UAVStructTy = DM.GetModule()->getTypeByName(PIXStructTypeName); @@ -295,10 +325,8 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, UAVStructTy = llvm::StructType::create(Elements, PIXStructTypeName); } - // Since this function should only be called once per module, - // we can modify the root sig at the same time: - AddUAVToDxilDefinedGlobalRootSignatures(DM); - AddUAVToShaderAttributeRootSignature(DM); + AddUAVToDxilDefinedGlobalRootSignatures(DM, hlslBindIndex); + AddUAVToShaderAttributeRootSignature(DM, hlslBindIndex); unsigned int Id = static_cast(DM.GetUAVs().size()); std::unique_ptr pUAV = llvm::make_unique(); @@ -320,8 +348,7 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, } pUAV->SetGlobalName(name); pUAV->SetRW(true); // sets UAV class - pUAV->SetSpaceID( - (unsigned int)-2); // This is the reserved-for-tools register space + pUAV->SetSpaceID(toolsRegisterSpace); // reserved-for-tools register space pUAV->SetSampleCount(0); // This is what compiler generates for a raw UAV pUAV->SetGloballyCoherent(false); pUAV->SetReorderCoherent(false); @@ -351,9 +378,17 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, auto *ret = pUAV.get(); DM.AddUAV(std::move(pUAV)); + DM.CollectShaderFlagsForModule(); return ret; } +void EraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction) { + if (OpFunction != nullptr && OpFunction->user_empty()) { + DM.GetOP()->RemoveFunction(OpFunction); + OpFunction->eraseFromParent(); + } +} + // Set up a UAV with structure of a single int llvm::CallInst *CreateUAVOnceForModule(hlsl::DxilModule &DM, llvm::IRBuilder<> &Builder, @@ -401,18 +436,6 @@ hlsl::DXIL::ShaderKind GetFunctionShaderKind(hlsl::DxilModule &DM, return shaderKind; } -std::vector GetAllBlocks(hlsl::DxilModule &DM) { - std::vector ret; - auto entryPoints = DM.GetExportedFunctions(); - for (auto &fn : entryPoints) { - auto &blocks = fn->getBasicBlockList(); - for (auto &block : blocks) { - ret.push_back(&block); - } - } - return ret; -} - ExpandedStruct ExpandStructType(LLVMContext &Ctx, Type *OriginalPayloadStructType) { SmallVector Elements; @@ -547,6 +570,24 @@ void ForEachDynamicallyIndexedResource( auto CreateHandleFn = HlslOP->GetOpFunc(DXIL::OpCode::CreateHandle, Type::getVoidTy(Ctx)); + auto CreateHandleFromBindingFn = HlslOP->GetOpFunc( + DXIL::OpCode::CreateHandleFromBinding, Type::getVoidTy(Ctx)); + auto CreateHandleFromHeapFn = HlslOP->GetOpFunc( + DXIL::OpCode::CreateHandleFromHeap, Type::getVoidTy(Ctx)); + + struct UnusedDeclarationCleanup { + hlsl::DxilModule &DM; + llvm::Function *CreateHandleFn; + llvm::Function *CreateHandleFromBindingFn; + llvm::Function *CreateHandleFromHeapFn; + ~UnusedDeclarationCleanup() { + EraseIfUnused(DM, CreateHandleFn); + EraseIfUnused(DM, CreateHandleFromBindingFn); + EraseIfUnused(DM, CreateHandleFromHeapFn); + } + } cleanup{DM, CreateHandleFn, CreateHandleFromBindingFn, + CreateHandleFromHeapFn}; + for (auto FI = CreateHandleFn->user_begin(); FI != CreateHandleFn->user_end();) { auto *FunctionUser = *FI++; @@ -562,8 +603,6 @@ void ForEachDynamicallyIndexedResource( } } - auto CreateHandleFromBindingFn = HlslOP->GetOpFunc( - DXIL::OpCode::CreateHandleFromBinding, Type::getVoidTy(Ctx)); for (auto FI = CreateHandleFromBindingFn->user_begin(); FI != CreateHandleFromBindingFn->user_end();) { auto *FunctionUser = *FI++; @@ -579,8 +618,6 @@ void ForEachDynamicallyIndexedResource( } } - auto CreateHandleFromHeapFn = HlslOP->GetOpFunc( - DXIL::OpCode::CreateHandleFromHeap, Type::getVoidTy(Ctx)); for (auto FI = CreateHandleFromHeapFn->user_begin(); FI != CreateHandleFromHeapFn->user_end();) { auto *FunctionUser = *FI++; diff --git a/lib/DxilPIXPasses/PixPassHelpers.h b/lib/DxilPIXPasses/PixPassHelpers.h index d7b0b40af8..3d6e24d22f 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.h +++ b/lib/DxilPIXPasses/PixPassHelpers.h @@ -48,7 +48,7 @@ llvm::CallInst *CreateHandleForResource(hlsl::DxilModule &DM, hlsl::DxilResourceBase *resource, const char *name); llvm::Function *GetEntryFunction(hlsl::DxilModule &DM); -std::vector GetAllBlocks(hlsl::DxilModule &DM); +void EraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction); std::vector GetAllInstrumentableFunctions(hlsl::DxilModule &DM); hlsl::DXIL::ShaderKind GetFunctionShaderKind(hlsl::DxilModule &DM, diff --git a/tools/clang/test/HLSLFileCheck/pix/pixelCounterEarlyZ.hlsl b/tools/clang/test/HLSLFileCheck/pix/pixelCounterEarlyZ.hlsl index 86d6d498b8..63968a3375 100644 --- a/tools/clang/test/HLSLFileCheck/pix/pixelCounterEarlyZ.hlsl +++ b/tools/clang/test/HLSLFileCheck/pix/pixelCounterEarlyZ.hlsl @@ -3,9 +3,11 @@ // Check the write to the UAV was emitted: // CHECK: %UAVIncResult = call i32 @dx.op.atomicBinOp.i32(i32 78, %dx.types.Handle %PIX_CountUAV_Handle, i32 0, i32 %ByteIndex, i32 undef, i32 undef, i32 1) -// Early z flag value is 8. The flags are stored in an entry in the entry function description record. See: +// The flags are stored in an entry in the entry function description record. See: // https://github.com/Microsoft/DirectXShaderCompiler/blob/main/docs/DXIL.rst#shader-properties-and-capabilities -// CHECK: !{i32 0, i64 8} +// 8 is force-early-z. 16 is EnableRawAndStructuredBuffers, set for the +// RWByteAddressBuffer counter this pass adds. 8 | 16 = 24. +// CHECK: !{i32 0, i64 24} float4 main(float4 pos : SV_Position) : SV_Target { return pos; diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index e21546c9ac..7f1b4cf365 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -33,7 +33,10 @@ #include #endif +#include "dxc/DXIL/DxilConstants.h" #include "dxc/DXIL/DxilModule.h" +#include "dxc/DXIL/DxilOperations.h" +#include "dxc/DXIL/DxilSubobject.h" #include "dxc/Test/DxcTestUtils.h" #include "dxc/Test/HLSLTestData.h" @@ -65,6 +68,7 @@ #include #include <../lib/DxilDia/DxcPixLiveVariables_FragmentIterator.h> +#include <../lib/DxilPIXPasses/PixPassHelpers.h> #include #include "PixTestUtils.h" @@ -143,6 +147,16 @@ class PixTest : public ::testing::Test { TEST_METHOD(RootSignatureUpgrade_SubObjects) TEST_METHOD(RootSignatureUpgrade_Annotation) + TEST_METHOD(ToolsUav_TwoPixPassesShareOneResource) + TEST_METHOD(ToolsUav_LibraryWithTwoEntryPointsCreatesOnePair) + TEST_METHOD(ToolsUav_ExtendsEveryGlobalRootSignatureSubobject) + TEST_METHOD(DebugInstrumentation_RawBufferShaderFlagDeclared) + TEST_METHOD(ToolsUav_RootSignatureSerializationFailurePreservesSignature) + TEST_METHOD(ConstantColor_UnusedIntOverloadIsErased) + TEST_METHOD(ConstantColor_NoTargetOverloadsAreErased) + TEST_METHOD(RemoveDiscards_UnusedDiscardOverloadIsErased) + TEST_METHOD(OperationCacheCleanup_RemovesErasedFunctions) + TEST_METHOD(DynamicResourceCleanup_VisitorStopsEarly) TEST_METHOD(DxilPIXDXRInvocationsLog_SanityTest) TEST_METHOD(DxilPIXDXRInvocationsLog_EmbeddedRootSigs) @@ -157,6 +171,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(DebugBreakInstrumentation_Multiple) TEST_METHOD(NonUniformResourceIndex_Resource) + TEST_METHOD(NonUniformResourceIndex_QualifiedCleanupValidates) TEST_METHOD(NonUniformResourceIndex_DescriptorHeap) TEST_METHOD(NonUniformResourceIndex_Raytracing) @@ -283,6 +298,7 @@ class PixTest : public ::testing::Test { std::vector Options; Options.push_back(L"-opt-mod-passes"); Options.push_back(passOption); + Options.push_back(L"-hlsl-dxilemit"); CComPtr pOptimizedModule; CComPtr pText; @@ -528,6 +544,12 @@ class PixTest : public ::testing::Test { } void ValidateAccessTrackingMods(const char *hlsl, bool modsExpected); + void LoadSubobjectsFromContainerIntoModule(IDxcBlob *container, + DxilModule &DM); + void VerifyGlobalRootSignaturesHaveToolsUAVs( + DxilSubobjects *subObjects, + const std::vector &expectedRootSignatureNames, + const std::vector &expectedShaderRegisters); class ModuleAndHangersOn { std::unique_ptr llvmContext; @@ -611,10 +633,11 @@ class PixTest : public ::testing::Test { void ValidateAllocaWrite(std::vector const &allocaWrites, size_t index, const char *name); PassOutput RunShaderAccessTrackingPass(IDxcBlob *blob); - std::string RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob); + CComPtr + RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob); CComPtr RunDxilPIXMeshShaderOutputPass(IDxcBlob *blob); CComPtr RunDxilPIXDXRInvocationsLog(IDxcBlob *blob); - std::vector + PassOutput RunDxilNonUniformResourceIndexInstrumentation(IDxcBlob *blob, std::string &outputText); void TestNuriCase(const char *source, const wchar_t *target, @@ -632,6 +655,122 @@ bool PixTest::InitSupport() { return true; } +static unsigned CountToolsUAVs(DxilModule &DM) { + unsigned count = 0; + for (auto const &uav : DM.GetUAVs()) { + if (uav->GetSpaceID() == static_cast(-2)) { + count++; + } + } + return count; +} + +static int CountToolsUAVRecords(std::vector const &lines) { + int count = 0; + for (auto const &line : lines) { + if (!line.empty() && line[0] == '!' && + line.find(", i32 -2, i32 ") != std::string::npos) { + count++; + } + } + return count; +} + +static bool +RootSignatureHasToolsUAV(const DxilVersionedRootSignatureDesc *rootSignature, + uint32_t shaderRegister) { + switch (rootSignature->Version) { + case DxilRootSignatureVersion::Version_1_0: { + const DxilRootSignatureDesc &desc = rootSignature->Desc_1_0; + for (uint32_t i = 0; i < desc.NumParameters; ++i) { + const DxilRootParameter ¶m = desc.pParameters[i]; + if (param.ParameterType == DxilRootParameterType::UAV && + param.Descriptor.RegisterSpace == static_cast(-2) && + param.Descriptor.ShaderRegister == shaderRegister) { + return true; + } + } + break; + } + case DxilRootSignatureVersion::Version_1_1: { + const DxilRootSignatureDesc1 &desc = rootSignature->Desc_1_1; + for (uint32_t i = 0; i < desc.NumParameters; ++i) { + const DxilRootParameter1 ¶m = desc.pParameters[i]; + if (param.ParameterType == DxilRootParameterType::UAV && + param.Descriptor.RegisterSpace == static_cast(-2) && + param.Descriptor.ShaderRegister == shaderRegister) { + return true; + } + } + break; + } + } + return false; +} + +void PixTest::LoadSubobjectsFromContainerIntoModule(IDxcBlob *container, + DxilModule &DM) { + const char *blobContent = + reinterpret_cast(container->GetBufferPointer()); + const unsigned blobSize = container->GetBufferSize(); + const hlsl::DxilContainerHeader *containerHeader = + hlsl::IsDxilContainerLike(blobContent, blobSize); + VERIFY_ARE_NOT_EQUAL(containerHeader, nullptr); + + const hlsl::DxilPartHeader *partHeader = + GetDxilPartByType(containerHeader, hlsl::DFCC_RuntimeData); + VERIFY_ARE_NOT_EQUAL(partHeader, nullptr); + + hlsl::RDAT::DxilRuntimeData rdat(GetDxilPartData(partHeader), + partHeader->PartSize); + std::unique_ptr subObjects(new DxilSubobjects()); + VERIFY_IS_TRUE(LoadSubobjectsFromRDAT(*subObjects, rdat)); + DM.ResetSubobjects(subObjects.release()); +} + +void PixTest::VerifyGlobalRootSignaturesHaveToolsUAVs( + DxilSubobjects *subObjects, + const std::vector &expectedRootSignatureNames, + const std::vector &expectedShaderRegisters) { + VERIFY_IS_NOT_NULL(subObjects); + + std::map foundRootSignatures; + for (const std::string &rootSignatureName : expectedRootSignatureNames) { + foundRootSignatures[rootSignatureName] = false; + } + + for (auto const &subObject : subObjects->GetSubobjects()) { + if (subObject.second->GetKind() != + hlsl::DXIL::SubobjectKind::GlobalRootSignature) { + continue; + } + + const std::string subObjectName = subObject.first.str(); + if (foundRootSignatures.find(subObjectName) == foundRootSignatures.end()) { + continue; + } + + const void *data = nullptr; + uint32_t size = 0; + constexpr bool notALocalRS = false; + VERIFY_IS_TRUE( + subObject.second->GetRootSignature(notALocalRS, data, size, nullptr)); + + DxilVersionedRootSignatureDesc const *rootSignature = nullptr; + DeserializeRootSignature(data, size, &rootSignature); + for (uint32_t expectedShaderRegister : expectedShaderRegisters) { + VERIFY_IS_TRUE( + RootSignatureHasToolsUAV(rootSignature, expectedShaderRegister)); + } + DeleteRootSignature(rootSignature); + foundRootSignatures[subObjectName] = true; + } + + for (const auto &foundRootSignature : foundRootSignatures) { + VERIFY_IS_TRUE(foundRootSignature.second); + } +} + void PixTest::TestPixUAVCase(char const *hlsl, wchar_t const *model, wchar_t const *entry) { auto mod = Compile(m_dllSupport, hlsl, model, {}, entry); @@ -846,7 +985,7 @@ CComPtr PixTest::RunDxilPIXDXRInvocationsLog(IDxcBlob *blob) { return pOptimizedModule; } -std::vector PixTest::RunDxilNonUniformResourceIndexInstrumentation( +PassOutput PixTest::RunDxilNonUniformResourceIndexInstrumentation( IDxcBlob *blob, std::string &outputText) { CComPtr dxil = FindModule(DFCC_ShaderDebugInfoDXIL, blob); @@ -865,11 +1004,13 @@ std::vector PixTest::RunDxilNonUniformResourceIndexInstrumentation( outputText = BlobToUtf8(pText); - const std::string disassembly = Disassemble(pOptimizedModule); - return Tokenize(disassembly, "\n"); + PassOutput result; + result.blob = pOptimizedModule; + result.lines = Tokenize(Disassemble(pOptimizedModule), "\n"); + return result; } -std::string +CComPtr PixTest::RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob) { CComPtr dxil = FindModule(DFCC_ShaderDebugInfoDXIL, blob); CComPtr pOptimizer; @@ -885,14 +1026,16 @@ PixTest::RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob) { VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); - std::string outputText; - if (pText->GetBufferSize() != 0) { - outputText = reinterpret_cast(pText->GetBufferPointer()); - } - - return outputText; + return pOptimizedModule; } +static bool HasDeclaration(const std::string &disassembly, + const std::string &functionName); +static std::string FindDeclarationLine(const std::string &disassembly, + const std::string &functionName); +static bool HasDeclarationLine(const std::string &disassembly, + const std::string &declaration); + TEST_F(PixTest, AddToASPayload) { const char *hlsl = R"( @@ -934,10 +1077,29 @@ void MSMain( )"; auto as = Compile(m_dllSupport, hlsl, L"as_6_6", {}, L"ASMain"); - RunDxilPIXAddTidToAmplificationShaderPayloadPass(as); + const std::string originalDispatchMeshDeclaration = + FindDeclarationLine(Disassemble(as), "dx.op.dispatchMesh"); + VERIFY_IS_FALSE(originalDispatchMeshDeclaration.empty()); + + auto asOutput = RunDxilPIXAddTidToAmplificationShaderPayloadPass(as); + VERIFY_IS_FALSE(HasDeclarationLine(Disassemble(asOutput), + originalDispatchMeshDeclaration)); auto ms = Compile(m_dllSupport, hlsl, L"ms_6_6", {}, L"MSMain"); - RunDxilPIXMeshShaderOutputPass(ms); + const std::string originalGetMeshPayloadDeclaration = + FindDeclarationLine(Disassemble(ms), "dx.op.getMeshPayload"); + VERIFY_IS_FALSE(originalGetMeshPayloadDeclaration.empty()); + + auto msOutput = RunDxilPIXMeshShaderOutputPass(ms); + const std::string meshDisassembly = Disassemble(msOutput); + VERIFY_IS_FALSE( + HasDeclarationLine(meshDisassembly, originalGetMeshPayloadDeclaration)); + VERIFY_IS_FALSE( + HasDeclaration(meshDisassembly, "dx.op.storeVertexOutput.i32")); + VERIFY_IS_FALSE( + HasDeclaration(meshDisassembly, "dx.op.storeVertexOutput.i16")); + VERIFY_IS_FALSE( + HasDeclaration(meshDisassembly, "dx.op.storeVertexOutput.f16")); } unsigned FindOrAddVSInSignatureElementForInstanceOrVertexID( hlsl::DxilSignature &InputSignature, hlsl::DXIL::SemanticKind semanticKind); @@ -3015,6 +3177,373 @@ float4 main(int i : A, float j : B) : SV_TARGET VERIFY_IS_TRUE(foundGlobalRS); } +TEST_F(PixTest, ToolsUav_TwoPixPassesShareOneResource) { + const char *source = R"x( +RWByteAddressBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + output.Store(4 * tid.x, tid.x); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); + auto debugOutput = RunDebugPass(compiled); + auto accessOutput = RunShaderAccessTrackingPass(debugOutput.blob); + + ModuleAndHangersOn moduleEtc(accessOutput.blob); + VERIFY_ARE_EQUAL(1u, CountToolsUAVs(moduleEtc.GetDxilModule())); + VerifyInstrumentedModuleIsValid( + accessOutput.blob, + "debug instrumentation followed by shader access tracking"); +} + +TEST_F(PixTest, ToolsUav_LibraryWithTwoEntryPointsCreatesOnePair) { + const char *source = R"x( +struct [raypayload] MyPayload +{ + float2 barycentrics : read(caller) : write(caller,anyhit); + uint primitiveIndex : read(caller) : write(caller,anyhit); +}; + +[shader("miss")] +void MissOne(inout MyPayload payload) +{ + payload.primitiveIndex = 1; +} + +[shader("miss")] +void MissTwo(inout MyPayload payload) +{ + payload.primitiveIndex = 2; +} +)x"; + + auto compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); + auto output = RunDxilPIXDXRInvocationsLog(compiled); + + auto lines = Tokenize(Disassemble(output), "\n"); + VERIFY_ARE_EQUAL(2, CountToolsUAVRecords(lines)); +} + +TEST_F(PixTest, ToolsUav_ExtendsEveryGlobalRootSignatureSubobject) { + const char *source = R"x( +GlobalRootSignature firstRootSignature = {"CBV(b0)"}; +GlobalRootSignature secondRootSignature = {"SRV(t0)"}; + +SubobjectToExportsAssociation firstAssociation = +{ + "firstRootSignature", + "MyClosestHit" +}; + +SubobjectToExportsAssociation secondAssociation = +{ + "secondRootSignature", + "MyMiss" +}; + +struct MyPayload +{ + float4 color; +}; + +[shader("raygeneration")] +void MyRayGen() +{ +} + +[shader("closesthit")] +void MyClosestHit(inout MyPayload payload, + in BuiltInTriangleIntersectionAttributes attr) +{ +} + +[shader("miss")] +void MyMiss(inout MyPayload payload) +{ +} +)x"; + + auto compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); + ModuleAndHangersOn moduleEtc(compiled); + DxilModule &DM = moduleEtc.GetDxilModule(); + LoadSubobjectsFromContainerIntoModule(compiled, DM); + PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_CountUAV_Handle"); + PIXPassHelpers::CreateGlobalUAVResource(DM, 1, "PIX_LogUAV_Handle"); + + VerifyGlobalRootSignaturesHaveToolsUAVs( + DM.GetSubobjects(), {"firstRootSignature", "secondRootSignature"}, + {0, 1}); +} + +TEST_F(PixTest, DebugInstrumentation_RawBufferShaderFlagDeclared) { + const char *source = R"x( +[numthreads(1, 1, 1)] +void main(uint threadId : SV_DispatchThreadID) +{ +})x"; + + auto compiled = Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); + auto output = RunDebugPass(compiled); + auto lines = Tokenize(Disassemble(output.blob), "\n"); + + constexpr uint64_t EnableRawAndStructuredBuffers = 0x10; + bool foundShaderFlags = false; + uint64_t shaderFlags = 0; + const std::string tagPrefix = "!{i32 0, i64 "; + for (auto const &line : lines) { + const auto tagStart = line.find(tagPrefix); + if (tagStart == std::string::npos) { + continue; + } + shaderFlags = + strtoull(line.c_str() + tagStart + tagPrefix.length(), nullptr, 10); + foundShaderFlags = true; + break; + } + + VERIFY_IS_TRUE(foundShaderFlags); + VERIFY_ARE_EQUAL(EnableRawAndStructuredBuffers, + shaderFlags & EnableRawAndStructuredBuffers); + VerifyInstrumentedModuleIsValid(output.blob, + "debug instrumentation shader flags"); +} + +TEST_F(PixTest, ToolsUav_RootSignatureSerializationFailurePreservesSignature) { + const char *source = R"x( +[numthreads(1, 1, 1)] +void main() +{ +})x"; + + DxilDescriptorRange range = {}; + range.RangeType = DxilDescriptorRangeType::UAV; + range.NumDescriptors = 1; + range.BaseShaderRegister = 0; + range.RegisterSpace = static_cast(-2); + range.OffsetInDescriptorsFromTableStart = DxilDescriptorRangeOffsetAppend; + + DxilRootParameter parameter = {}; + parameter.ParameterType = DxilRootParameterType::DescriptorTable; + parameter.DescriptorTable.NumDescriptorRanges = 1; + parameter.DescriptorTable.pDescriptorRanges = ⦥ + parameter.ShaderVisibility = DxilShaderVisibility::All; + + DxilVersionedRootSignatureDesc rootSignature = {}; + rootSignature.Version = DxilRootSignatureVersion::Version_1_0; + rootSignature.Desc_1_0.NumParameters = 1; + rootSignature.Desc_1_0.pParameters = ¶meter; + rootSignature.Desc_1_0.Flags = DxilRootSignatureFlags::None; + + CComPtr serializedRootSignature; + CComPtr errorBlob; + SerializeRootSignature(&rootSignature, &serializedRootSignature, &errorBlob, + true); + VERIFY_IS_NOT_NULL(serializedRootSignature); + + auto serializedData = + static_cast(serializedRootSignature->GetBufferPointer()); + std::vector originalRootSignature( + serializedData, + serializedData + serializedRootSignature->GetBufferSize()); + + auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); + ModuleAndHangersOn moduleEtc(compiled); + DxilModule &DM = moduleEtc.GetDxilModule(); + DM.ResetSerializedRootSignature(originalRootSignature); + + std::unique_ptr subObjects(new DxilSubobjects()); + constexpr bool notALocalRootSignature = false; + subObjects->CreateRootSignature( + "testRootSignature", notALocalRootSignature, originalRootSignature.data(), + static_cast(originalRootSignature.size())); + DM.ResetSubobjects(subObjects.release()); + + PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_TestUAV"); + + const std::vector &actualRootSignature = + DM.GetSerializedRootSignature(); + VERIFY_ARE_EQUAL(originalRootSignature.size(), actualRootSignature.size()); + VERIFY_IS_TRUE(std::equal(originalRootSignature.begin(), + originalRootSignature.end(), + actualRootSignature.begin())); + + bool foundRootSignature = false; + for (auto const &subObject : DM.GetSubobjects()->GetSubobjects()) { + if (subObject.first != "testRootSignature") { + continue; + } + + const void *data = nullptr; + uint32_t size = 0; + VERIFY_IS_TRUE(subObject.second->GetRootSignature(notALocalRootSignature, + data, size, nullptr)); + VERIFY_ARE_EQUAL(originalRootSignature.size(), static_cast(size)); + VERIFY_IS_TRUE(std::equal(originalRootSignature.begin(), + originalRootSignature.end(), + static_cast(data))); + foundRootSignature = true; + } + VERIFY_IS_TRUE(foundRootSignature); +} + +static bool HasUnusedDeclaration(std::vector const &lines, + std::string const &functionName) { + bool declared = false; + for (auto const &line : lines) { + if (line.find("declare") != std::string::npos && + line.find(functionName) != std::string::npos) { + declared = true; + } + if (line.find("call") != std::string::npos && + line.find(functionName) != std::string::npos) { + return false; + } + } + return declared; +} + +static bool HasDeclaration(const std::string &disassembly, + const std::string &functionName) { + for (const std::string &line : Tokenize(disassembly, "\n")) { + if (line.find("declare") != std::string::npos && + line.find(functionName) != std::string::npos) { + return true; + } + } + return false; +} + +static std::string FindDeclarationLine(const std::string &disassembly, + const std::string &functionName) { + for (const std::string &line : Tokenize(disassembly, "\n")) { + if (line.find("declare") != std::string::npos && + line.find(functionName) != std::string::npos) { + return line; + } + } + return {}; +} + +static bool HasDeclarationLine(const std::string &disassembly, + const std::string &declaration) { + for (const std::string &line : Tokenize(disassembly, "\n")) { + if (line == declaration) { + return true; + } + } + return false; +} + +TEST_F(PixTest, ConstantColor_UnusedIntOverloadIsErased) { + const char *source = R"x( +float4 main() : SV_Target +{ + return float4(1, 2, 3, 4); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); + + VERIFY_IS_FALSE(HasUnusedDeclaration(output.Lines, "dx.op.storeOutput.i32")); + VerifyInstrumentedModuleIsValid(output.Module, + "constant-colour substitution"); +} + +TEST_F(PixTest, ConstantColor_NoTargetOverloadsAreErased) { + const char *source = R"x( +[numthreads(1, 1, 1)] +void main() +{ +})x"; + + auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); + const std::string disassembly = Disassemble(output.Module); + + VerifyInstrumentedModuleIsValid( + output.Module, "constant-colour substitution with no target"); + VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.storeOutput.f32")); + VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.storeOutput.i32")); +} + +TEST_F(PixTest, RemoveDiscards_UnusedDiscardOverloadIsErased) { + const char *source = R"x( +float4 main() : SV_Target +{ + return float4(1, 2, 3, 4); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-hlsl-dxil-remove-discards"); + + VERIFY_IS_FALSE(HasUnusedDeclaration(output.Lines, "dx.op.discard")); + VerifyInstrumentedModuleIsValid(output.Module, + "discard removal with no discard"); +} + +TEST_F(PixTest, OperationCacheCleanup_RemovesErasedFunctions) { + const char *source = R"x( +float4 main() : SV_Target +{ + return float4(1, 2, 3, 4); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + ModuleAndHangersOn moduleEtc(compiled); + DxilModule &DM = moduleEtc.GetDxilModule(); + OP *HlslOP = DM.GetOP(); + llvm::Function *discard = + HlslOP->GetOpFunc(DXIL::OpCode::Discard, + llvm::Type::getVoidTy(DM.GetModule()->getContext())); + + VERIFY_ARE_EQUAL(1u, + static_cast( + HlslOP->GetOpFuncList(DXIL::OpCode::Discard).size())); + PIXPassHelpers::EraseIfUnused(DM, discard); + VERIFY_ARE_EQUAL(0u, + static_cast( + HlslOP->GetOpFuncList(DXIL::OpCode::Discard).size())); + + llvm::Function *recreated = + HlslOP->GetOpFunc(DXIL::OpCode::Discard, + llvm::Type::getVoidTy(DM.GetModule()->getContext())); + VERIFY_IS_NOT_NULL(recreated); + PIXPassHelpers::EraseIfUnused(DM, recreated); +} + +TEST_F(PixTest, DynamicResourceCleanup_VisitorStopsEarly) { + const char *source = R"x( +Texture2D textures[] : register(t0); + +float4 main(float2 uv : TEXCOORD0) : SV_Target +{ + return textures[(uint)uv.x].Load(int3(0, 0, 0)); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + ModuleAndHangersOn moduleEtc(compiled); + DxilModule &DM = moduleEtc.GetDxilModule(); + bool visitorCalled = false; + PIXPassHelpers::ForEachDynamicallyIndexedResource( + DM, [&visitorCalled](bool, llvm::Instruction *, llvm::Value *) { + visitorCalled = true; + return false; + }); + + VERIFY_IS_TRUE(visitorCalled); + OP *HlslOP = DM.GetOP(); + VERIFY_ARE_EQUAL( + 0u, + static_cast( + HlslOP->GetOpFuncList(DXIL::OpCode::CreateHandleFromBinding).size())); + VERIFY_ARE_EQUAL( + 0u, + static_cast( + HlslOP->GetOpFuncList(DXIL::OpCode::CreateHandleFromHeap).size())); +} + TEST_F(PixTest, DxilPIXDXRInvocationsLog_SanityTest) { const char *source = R"x( @@ -3108,8 +3637,9 @@ void PixTest::TestNuriCase(const char *source, const wchar_t *target, Compile(m_dllSupport, source, target, compilationOptions); std::string outputText; - const std::vector dxilLines = + PassOutput output = RunDxilNonUniformResourceIndexInstrumentation(compiledLib, outputText); + const std::vector &dxilLines = output.lines; VERIFY_ARE_EQUAL(NuriGetWaveInstructionCount(dxilLines), expectedResult); @@ -3155,6 +3685,33 @@ float4 main(float2 uv : TEXCOORD0) : SV_TARGET TestNuriCase(sourceWithNuri, L"ps_6_6", 0); } +TEST_F(PixTest, NonUniformResourceIndex_QualifiedCleanupValidates) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *source = R"x( +Texture2D textures[] : register(t0); + +float4 main(float2 uv : TEXCOORD0) : SV_Target +{ + uint index = (uint)uv.x; + return textures[NonUniformResourceIndex(index)].Load(int3(0, 0, 0)); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); + std::string outputText; + PassOutput output = + RunDxilNonUniformResourceIndexInstrumentation(compiled, outputText); + const std::string disassembly = Disassemble(output.blob); + + VerifyInstrumentedModuleIsValid( + output.blob, "qualified non-uniform resource index instrumentation"); + VERIFY_ARE_EQUAL(0u, NuriGetWaveInstructionCount(output.lines)); + VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.waveActiveAllEqual.i32")); + VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.atomicBinOp.i32")); +} + TEST_F(PixTest, NonUniformResourceIndex_DescriptorHeap) { if (m_ver.SkipDxilVersion(1, 6)) { @@ -3551,15 +4108,15 @@ void main() { } TEST_F(PixTest, DebugBreakInstrumentation_NoDebugBreak) { + if (m_ver.SkipDxilVersion(1, 10)) + return; const char *source = R"x( -RWByteAddressBuffer buf : register(u0); [numthreads(1, 1, 1)] void main() { - buf.Store(0, 1); })x"; - auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); + auto compiled = Compile(m_dllSupport, source, L"cs_6_10", {}); auto output = RunDebugBreakPass(compiled); bool foundDebugBreak = false; for (auto const &line : output.lines) { @@ -3567,6 +4124,8 @@ void main() { foundDebugBreak = true; } VERIFY_IS_FALSE(foundDebugBreak); + VerifyInstrumentedModuleIsValid(output.blob, + "debug-break instrumentation with no call"); } TEST_F(PixTest, DebugBreakInstrumentation_Multiple) { From 9976585da04fe55530cad34b873f73ffcefc1fed Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 18:54:08 -0700 Subject: [PATCH 03/39] [PIX] Skip NURI handles with no instruction number The non-uniform resource index pass writes each diagnostic bit at an address that it computes from the PIX instruction number of the handle. If a handle has no instruction number, the value stays at 0. A debug build stops at an assertion. A release build writes the bit for instruction 0 and gives the diagnostic to an unrelated instruction. Library helper functions are the usual source of these handles. The pass ignores such a handle instead of guessing. An absent record is correct. A record at instruction 0 is wrong. The report NuriNotInstrumentedMissingInstructionNumber tells the caller that the data is not complete. The pass reads the instruction number before it makes the UAV. A module whose dynamic handles all lack an instruction number therefore gets no UAV that it cannot use. A PIX build that does not read the new report keeps its current behavior. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- ...NonUniformResourceIndexInstrumentation.cpp | 25 +++++++++++++----- ...UniformResourceIndexInstructionNumber.hlsl | 23 ++++++++++++++++ .../NonUniformResourceIndexLibraryHelper.hlsl | 26 +++++++++++++++++++ ...formResourceIndexNoInstructionNumbers.hlsl | 22 ++++++++++++++++ 4 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexInstructionNumber.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexLibraryHelper.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexNoInstructionNumbers.hlsl diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index 3c7a542648..d1e7fa0fd8 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -59,6 +59,10 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { std::map FunctionToUAVHandle; + // Set if any dynamically indexed handle lacks the PIX instruction ordinal + // this pass needs to address its diagnostic. + bool FoundHandleWithoutInstructionNumber = false; + // This is the main pass that will iterate through all of the resources that // are dynamically indexed. If not already marked NonUniformResourceIndex, // then insert WaveActiveAllEqual to determine if the index is uniform @@ -72,6 +76,16 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { return true; } + // Address each diagnostic by the PIX instruction ordinal. Skip a + // handle that has no ordinal instead of writing a record for + // instruction 0. + uint32_t InstructionNumber = 0; + if (!pix_dxil::PixDxilInstNum::FromInst(CreateHandle, + &InstructionNumber)) { + FoundHandleWithoutInstructionNumber = true; + return true; + } + if (!PixUAVResource) { PixUAVResource = PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PixUAVResource"); @@ -97,12 +111,6 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { IRBuilder<> Builder(CreateHandle); - uint32_t InstructionNumber = 0; - if (!pix_dxil::PixDxilInstNum::FromInst(CreateHandle, - &InstructionNumber)) { - DXASSERT_NOMSG(false); - } - // The output UAV is treated as a bit array where each bit corresponds // to an instruction number. This determines what byte offset to write // our result to based on the instruction number. @@ -161,6 +169,11 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { } } + if (FoundHandleWithoutInstructionNumber && OSOverride != nullptr) { + formatted_raw_ostream FOS(*OSOverride); + FOS << "\nNuriNotInstrumentedMissingInstructionNumber\n"; + } + return modified; } diff --git a/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexInstructionNumber.hlsl b/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexInstructionNumber.hlsl new file mode 100644 index 0000000000..d002fed467 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexInstructionNumber.hlsl @@ -0,0 +1,23 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -dxil-annotate-with-virtual-regs -hlsl-dxil-non-uniform-resource-index-instrumentation | %FileCheck %s + +// With the annotation prepass in place, the diagnostic is addressed to +// the ordinal of the createHandle that performed the unmarked dynamic +// indexing. The pass encodes that ordinal as a shift. A shift of zero +// aliases the diagnostic onto bit 0. +// +// Match any non-zero shift rather than a literal ordinal. A createHandle +// whose index comes from an interpolated input is never the first +// numbered instruction. + +// CHECK-NOT: NuriNotInstrumentedMissingInstructionNumber +// CHECK: @dx.op.waveActiveAllEqual +// CHECK: shl i32 %{{[0-9]+}}, {{[1-9][0-9]*}} +// CHECK: @dx.op.atomicBinOp.i32(i32 78 + +Texture2D tex[8] : register(t0); + +float4 main(float2 uv : TEXCOORD0) : SV_TARGET +{ + uint index = uv.x * uv.y; + return tex[index].Load(int3(0, 0, 0)); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexLibraryHelper.hlsl b/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexLibraryHelper.hlsl new file mode 100644 index 0000000000..348385b083 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexLibraryHelper.hlsl @@ -0,0 +1,26 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -dxil-annotate-with-virtual-regs -hlsl-dxil-non-uniform-resource-index-instrumentation | %FileCheck %s + +// Coverage for an unmarked dynamic index that stays in a library helper. +// The helper remains a separate function. The diagnostic is addressed to +// a non-zero instruction ordinal. + +// CHECK-NOT: NuriNotInstrumentedMissingInstructionNumber +// CHECK: define void {{.*}}IndexInHelper +// CHECK: @dx.op.waveActiveAllEqual +// CHECK: shl i32 %{{[0-9]+}}, {{[1-9][0-9]*}} +// CHECK: @dx.op.atomicBinOp.i32(i32 78 + +RWTexture2D RT[] : register(u0); + +[noinline] +export void IndexInHelper(uint index) +{ + float2 rayIndex = DispatchRaysIndex().xy; + RT[index][rayIndex] = 1; +} + +[shader("raygeneration")] +void RayGen() +{ + IndexInHelper(DispatchRaysIndex().x); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexNoInstructionNumbers.hlsl b/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexNoInstructionNumbers.hlsl new file mode 100644 index 0000000000..d810401b4e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/NonUniformResourceIndexNoInstructionNumbers.hlsl @@ -0,0 +1,22 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-non-uniform-resource-index-instrumentation | %FileCheck %s + +// This pass addresses each diagnostic by the PIX instruction ordinal. +// This RUN line omits the annotation prepass, so no createHandle carries +// an ordinal. The pass leaves the handle uninstrumented and reports the +// missing precondition. +// +// The pass writes its messages to the same stream as the -S module print, +// and writes them before the module, so the message checks come first. + +// CHECK-NOT: FoundDynamicIndexingNoNuri +// CHECK: NuriNotInstrumentedMissingInstructionNumber +// CHECK-NOT: @dx.op.waveActiveAllEqual +// CHECK-NOT: @dx.op.atomicBinOp + +Texture2D tex[8] : register(t0); + +float4 main(float2 uv : TEXCOORD0) : SV_TARGET +{ + uint index = uv.x * uv.y; + return tex[index].Load(int3(0, 0, 0)); +} From d6de1e3d57def9c264fc2da7327a6933a29cbcca Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 18:07:58 -0700 Subject: [PATCH 04/39] [PIX] Fix DXR invocation log bounds Each DXR invocation takes one slot in the log with an atomic increment on the counter UAV. If the slot number is too large, the pass limits it to the last slot. Every invocation that overflows therefore writes over the last correct record. PIX sees a full log with a plausible final entry and cannot detect the truncation. A capacity of zero makes that clamp wrap to a very large value, so a log with no slots accepts writes. An out-of-range slot must produce no write. The correct records then stay unchanged, and the overflow stays visible, because the claimed count is larger than the capacity. An invocation that overflows contributes no record. A tool that treats a full log as a complete log must compare the claimed count with the capacity. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilPIXDXRInvocationsLog.cpp | 36 ++++---- .../pix/InvocationsLog_ClosestHit.hlsl | 6 +- .../InvocationsLog_OverflowIsNotClamped.hlsl | 57 +++++++++++++ tools/clang/unittests/HLSL/PixTest.cpp | 85 ++++++++++++++++++- 4 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/InvocationsLog_OverflowIsNotClamped.hlsl diff --git a/lib/DxilPIXPasses/DxilPIXDXRInvocationsLog.cpp b/lib/DxilPIXPasses/DxilPIXDXRInvocationsLog.cpp index c9f553a4b6..5e6b685554 100644 --- a/lib/DxilPIXPasses/DxilPIXDXRInvocationsLog.cpp +++ b/lib/DxilPIXPasses/DxilPIXDXRInvocationsLog.cpp @@ -18,6 +18,7 @@ #include "llvm/IR/InstIterator.h" #include "llvm/IR/PassManager.h" #include "llvm/Support/FormattedStream.h" +#include "llvm/Transforms/Utils/BasicBlockUtils.h" #include "llvm/Transforms/Utils/Local.h" #include "PixPassHelpers.h" @@ -66,6 +67,11 @@ bool DxilPIXDXRInvocationsLog::runOnModule(Module &M) { LLVMContext &Ctx = M.getContext(); OP *HlslOP = DM.GetOP(); + // A zero-entry log has no space for records. + if (m_MaxNumEntriesInLog == 0) { + return false; + } + bool Modified = false; for (auto entryFunction : DM.GetExportedFunctions()) { @@ -85,7 +91,9 @@ bool DxilPIXDXRInvocationsLog::runOnModule(Module &M) { Modified = true; - IRBuilder<> Builder(dxilutil::FirstNonAllocaInsertionPt(entryFunction)); + Instruction *InsertionPoint = + dxilutil::FirstNonAllocaInsertionPt(entryFunction); + IRBuilder<> Builder(InsertionPoint); // Add the UAVs that we're going to write to CallInst *HandleForCountUAV = PIXPassHelpers::CreateUAVOnceForModule( @@ -163,10 +171,6 @@ bool DxilPIXDXRInvocationsLog::runOnModule(Module &M) { Constant *AtomicAdd = HlslOP->GetU32Const((unsigned)DXIL::AtomicBinOpCode::Add); - Function *UMinOpFunc = - HlslOP->GetOpFunc(OP::OpCode::UMin, Type::getInt32Ty(Ctx)); - Constant *UMinOpCode = HlslOP->GetU32Const((unsigned)OP::OpCode::UMin); - Function *StoreFuncFloat = HlslOP->GetOpFunc(OP::OpCode::BufferStore, Type::getFloatTy(Ctx)); Function *StoreFuncInt = @@ -177,8 +181,8 @@ bool DxilPIXDXRInvocationsLog::runOnModule(Module &M) { Constant *WriteMask_XYZW = HlslOP->GetI8Const(15); Constant *WriteMask_X = HlslOP->GetI8Const(1); Constant *ShaderKindAsConstant = HlslOP->GetU32Const((uint32_t)ShaderKind); - Constant *MaxEntryIndexAsConstant = - HlslOP->GetU32Const((uint32_t)m_MaxNumEntriesInLog - 1u); + Constant *MaxEntryCountAsConstant = + HlslOP->GetU32Const((uint32_t)m_MaxNumEntriesInLog); Constant *Zero32Arg = HlslOP->GetU32Const(0); Constant *One32Arg = HlslOP->GetU32Const(1); UndefValue *UndefArg = UndefValue::get(Type::getInt32Ty(Ctx)); @@ -198,19 +202,21 @@ bool DxilPIXDXRInvocationsLog::runOnModule(Module &M) { }, "EntryIndexResult"); - // Clamp the index so that we don't write off the end of the UAV. If we - // clamp, then it's up to PIX to replay the work again with a larger log - // buffer. - auto *EntryIndexClamped = Builder.CreateCall( - UMinOpFunc, {UMinOpCode, EntryIndex, MaxEntryIndexAsConstant}); + // The counter keeps counting past the log capacity. Skip the stores once + // the claimed slot is out of range, so the recorded entries stay intact. + auto *EntryIndexIsInRange = Builder.CreateICmpULT( + EntryIndex, MaxEntryCountAsConstant, "EntryIndexIsInRange"); + TerminatorInst *StoreEntryBlockTerminator = + SplitBlockAndInsertIfThen(EntryIndexIsInRange, InsertionPoint, + /*Unreachable*/ false); + Builder.SetInsertPoint(StoreEntryBlockTerminator); const auto numBytesPerEntry = 4 + (3 * 4) + (3 * 4) + (3 * 4) + 4 + 4 + 4; // See number of bytes we store per shader invocation below - auto EntryOffset = - Builder.CreateMul(EntryIndexClamped, - HlslOP->GetU32Const(numBytesPerEntry), "EntryOffset"); + auto EntryOffset = Builder.CreateMul( + EntryIndex, HlslOP->GetU32Const(numBytesPerEntry), "EntryOffset"); auto EntryOffsetPlus16 = Builder.CreateAdd( EntryOffset, HlslOP->GetU32Const(16), "EntryOffsetPlus16"); auto EntryOffsetPlus32 = Builder.CreateAdd( diff --git a/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_ClosestHit.hlsl b/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_ClosestHit.hlsl index a17b94b153..34c932dd36 100644 --- a/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_ClosestHit.hlsl +++ b/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_ClosestHit.hlsl @@ -14,7 +14,7 @@ // Now check that at least three functions were modified (the hit group shaders): // -------- one ---------- -// Check for out-of-bounds clamp: +// Check for the per-entry offset calculation: // CHECK: mul i32 // CHECK: 52 @@ -28,7 +28,7 @@ // CHECK: i32 1 // -------- two ---------- -// Check for out-of-bounds clamp: +// Check for the per-entry offset calculation: // CHECK: mul i32 // CHECK: 52 @@ -43,7 +43,7 @@ // CHECK: dx.op.atomicBinOp.i32 // CHECK: i32 1 -// Check for out-of-bounds clamp: +// Check for the per-entry offset calculation: // CHECK: mul i32 // CHECK: 52 diff --git a/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_OverflowIsNotClamped.hlsl b/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_OverflowIsNotClamped.hlsl new file mode 100644 index 0000000000..8f61c7683e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/InvocationsLog_OverflowIsNotClamped.hlsl @@ -0,0 +1,57 @@ +// RUN: %dxc -Tlib_6_6 %s | %opt -S -hlsl-dxil-pix-dxr-invocations-log,maxNumEntriesInLog=100 | %FileCheck %s + +// Each invocation claims one log slot. +// The counter keeps counting past the log capacity. +// The stores execute only when the claimed slot is in range. + +// CHECK: [[ENTRYINDEX:%EntryIndexResult[0-9]*]] = call i32 @dx.op.atomicBinOp.i32(i32 78, +// CHECK: [[INRANGE:%EntryIndexIsInRange[0-9]*]] = icmp ult i32 [[ENTRYINDEX]], 100 +// CHECK: br i1 [[INRANGE]] +// CHECK: mul i32 [[ENTRYINDEX]], 52 +// CHECK: call void @dx.op.bufferStore.i32 +// CHECK: call void @dx.op.bufferStore.f32 +// CHECK: call void @dx.op.bufferStore.f32 +// CHECK: call void @dx.op.bufferStore.i32 + +// UMin is not part of this shader. +// CHECK-NOT: @dx.op.binary.i32(i32 40 +// CHECK-NOT: declare i32 @dx.op.binary.i32 + +struct Payload +{ + float4 color; +}; + +struct Attribs +{ + float2 barycentrics; +}; + +RaytracingAccelerationStructure scene : register(t0); +RWTexture2D output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + RayDesc ray; + ray.Origin = float3(0, 0, 0); + ray.Direction = float3(0, 0, 1); + ray.TMin = 0.001f; + ray.TMax = 1000.f; + Payload payload; + payload.color = float4(0, 0, 0, 0); + TraceRay(scene, RAY_FLAG_NONE, ~0, 0, 1, 0, ray, payload); + output[DispatchRaysIndex().xy] = payload.color; +} + +[shader("closesthit")] +void ClosestHit(inout Payload payload, in Attribs attribs) +{ + payload.color = float4(attribs.barycentrics, 0, 1); +} + +[shader("miss")] +void Miss(inout Payload payload) +{ + payload.color = float4(1, 0, 0, 1); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 7f1b4cf365..d455fcfea5 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -160,6 +160,10 @@ class PixTest : public ::testing::Test { TEST_METHOD(DxilPIXDXRInvocationsLog_SanityTest) TEST_METHOD(DxilPIXDXRInvocationsLog_EmbeddedRootSigs) + TEST_METHOD(DxilPIXDXRInvocationsLog_ZeroCapacityEmitsNothing) + TEST_METHOD(DxilPIXDXRInvocationsLog_OneEntryUsesEntryCountBound) + TEST_METHOD(DxilPIXDXRInvocationsLog_ExactCapacityUsesEntryCountBound) + TEST_METHOD(DxilPIXDXRInvocationsLog_OverflowGuardValidates) TEST_METHOD(DebugInstrumentation_TextOutput) TEST_METHOD(DebugInstrumentation_BlockReport) @@ -636,7 +640,8 @@ class PixTest : public ::testing::Test { CComPtr RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob); CComPtr RunDxilPIXMeshShaderOutputPass(IDxcBlob *blob); - CComPtr RunDxilPIXDXRInvocationsLog(IDxcBlob *blob); + CComPtr + RunDxilPIXDXRInvocationsLog(IDxcBlob *blob, unsigned maxNumEntriesInLog = 24); PassOutput RunDxilNonUniformResourceIndexInstrumentation(IDxcBlob *blob, std::string &outputText); @@ -676,6 +681,19 @@ static int CountToolsUAVRecords(std::vector const &lines) { return count; } +static bool +HasDxrInvocationLogEntryCountCheck(std::vector const &lines, + unsigned expectedEntryCount) { + const std::string expectedSuffix = ", " + std::to_string(expectedEntryCount); + for (auto const &line : lines) { + if (line.find("icmp ult i32 %EntryIndexResult") != std::string::npos && + line.find(expectedSuffix) != std::string::npos) { + return true; + } + } + return false; +} + static bool RootSignatureHasToolsUAV(const DxilVersionedRootSignatureDesc *rootSignature, uint32_t shaderRegister) { @@ -962,15 +980,19 @@ CComPtr PixTest::RunDxilPIXMeshShaderOutputPass(IDxcBlob *blob) { return pOptimizedModule; } -CComPtr PixTest::RunDxilPIXDXRInvocationsLog(IDxcBlob *blob) { +CComPtr +PixTest::RunDxilPIXDXRInvocationsLog(IDxcBlob *blob, + unsigned maxNumEntriesInLog) { CComPtr dxil = FindModule(DFCC_ShaderDebugInfoDXIL, blob); CComPtr pOptimizer; VERIFY_SUCCEEDED( m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::wstring logArg = L"-hlsl-dxil-pix-dxr-invocations-log," + L"maxNumEntriesInLog=" + + std::to_wstring(maxNumEntriesInLog); std::vector Options; - Options.push_back( - L"-hlsl-dxil-pix-dxr-invocations-log,maxNumEntriesInLog=24"); + Options.push_back(logArg.c_str()); CComPtr pOptimizedModule; CComPtr pText; @@ -985,6 +1007,20 @@ CComPtr PixTest::RunDxilPIXDXRInvocationsLog(IDxcBlob *blob) { return pOptimizedModule; } +static const char *kSingleMissInvocationLogShader = R"x( +struct [raypayload] MyPayload +{ + float2 barycentrics : read(caller) : write(caller,anyhit); + uint primitiveIndex : read(caller) : write(caller,anyhit); +}; + +[shader("miss")] +void MissOne(inout MyPayload payload) +{ + payload.primitiveIndex = 1; +} +)x"; + PassOutput PixTest::RunDxilNonUniformResourceIndexInstrumentation( IDxcBlob *blob, std::string &outputText) { @@ -3615,6 +3651,47 @@ void MyMiss(inout MyPayload payload) RunDxilPIXDXRInvocationsLog(compiledLib); } +TEST_F(PixTest, DxilPIXDXRInvocationsLog_ZeroCapacityEmitsNothing) { + auto compiledLib = + Compile(m_dllSupport, kSingleMissInvocationLogShader, L"lib_6_6", {}); + + auto oneEntryOutput = RunDxilPIXDXRInvocationsLog(compiledLib, 1); + auto oneEntryLines = Tokenize(Disassemble(oneEntryOutput), "\n"); + VERIFY_ARE_EQUAL(2, CountToolsUAVRecords(oneEntryLines)); + + auto zeroEntryOutput = RunDxilPIXDXRInvocationsLog(compiledLib, 0); + auto zeroEntryLines = Tokenize(Disassemble(zeroEntryOutput), "\n"); + VERIFY_ARE_EQUAL(0, CountToolsUAVRecords(zeroEntryLines)); +} + +TEST_F(PixTest, DxilPIXDXRInvocationsLog_OneEntryUsesEntryCountBound) { + auto compiledLib = + Compile(m_dllSupport, kSingleMissInvocationLogShader, L"lib_6_6", {}); + auto output = RunDxilPIXDXRInvocationsLog(compiledLib, 1); + auto lines = Tokenize(Disassemble(output), "\n"); + + VERIFY_IS_TRUE(HasDxrInvocationLogEntryCountCheck(lines, 1)); +} + +TEST_F(PixTest, DxilPIXDXRInvocationsLog_ExactCapacityUsesEntryCountBound) { + auto compiledLib = + Compile(m_dllSupport, kSingleMissInvocationLogShader, L"lib_6_6", {}); + auto output = RunDxilPIXDXRInvocationsLog(compiledLib, 24); + auto lines = Tokenize(Disassemble(output), "\n"); + + VERIFY_IS_TRUE(HasDxrInvocationLogEntryCountCheck(lines, 24)); +} + +TEST_F(PixTest, DxilPIXDXRInvocationsLog_OverflowGuardValidates) { + auto compiledLib = + Compile(m_dllSupport, kSingleMissInvocationLogShader, L"lib_6_6", {}); + auto output = RunDxilPIXDXRInvocationsLog(compiledLib, 1); + std::string disassembly = Disassemble(output); + + VERIFY_IS_TRUE(disassembly.find("@dx.op.binary.i32") == std::string::npos); + VerifyInstrumentedModuleIsValid(output, "DXR invocations log overflow guard"); +} + uint32_t NuriGetWaveInstructionCount(const std::vector &lines) { // This is the instruction we'll insert into the shader if we detect dynamic // resource indexing From fc840ef7710b6015ad6ed4862caee2025714ca28 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 18:54:07 -0700 Subject: [PATCH 05/39] [PIX] Recompute shader flags after NURI instrumentation The non-uniform resource index pass inserts WaveActiveAllEqual to test whether a dynamic index is uniform. It inserts these calls after the compiler computed the shader flags. The module can therefore declare no wave operations and still contain them, and the validator refuses that module. The pass computes the flags again, but only when it changed the module. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- ...NonUniformResourceIndexInstrumentation.cpp | 4 ++ tools/clang/unittests/HLSL/PixTest.cpp | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index d1e7fa0fd8..ef58abe355 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -161,6 +161,10 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { PIXPassHelpers::EraseIfUnused(DM, AtomicOpFunc); if (modified) { + // Recompute shader flags after inserting WaveActiveAllEqual so the + // declared flags match the module. + DM.CollectShaderFlagsForModule(); + DM.ReEmitDxilResources(); if (OSOverride != nullptr) { diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index d455fcfea5..239b6900be 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -184,6 +184,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(Validation_ControlValidModulePasses) TEST_METHOD(Validation_ControlInvalidModuleFails) TEST_METHOD(Validation_ControlBoilerplateOnlyFailureIsRejected) + TEST_METHOD(Validation_NonUniformResourceIndex_WaveOpsFlag) dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -4350,3 +4351,46 @@ TEST_F(PixTest, Validation_ControlBoilerplateOnlyFailureIsRejected) { VERIFY_IS_FALSE(realDiagnostic.Significant.empty()); VERIFY_IS_FALSE(IsPermittedValidationException(realDiagnostic)); } + +TEST_F(PixTest, Validation_NonUniformResourceIndex_WaveOpsFlag) { + const char *source = R"x( +Texture2D textures[] : register(t0); +SamplerState samp : register(s0); + +cbuffer Constants : register(b0) +{ + uint index; +}; + +float4 main(float4 pos : SV_Position) : SV_Target +{ + return textures[index].Sample(samp, pos.xy); +})x"; + + // This index is dynamic and unmarked, so the pass instruments it; an + // index already marked NonUniformResourceIndex would be skipped. + // Instrumentation inserts WaveActiveAllEqual, which requires the WaveOps + // shader flag. + auto compiled = Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); + CComPtr dxil = FindModule(DFCC_ShaderDebugInfoDXIL, compiled); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::array Options = { + L"-opt-mod-passes", L"-dxil-dbg-value-to-dbg-declare", + L"-dxil-annotate-with-virtual-regs", + L"-hlsl-dxil-non-uniform-resource-index-instrumentation"}; + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + VerifyInstrumentedModuleIsValid(pOptimizedModule, + "non-uniform resource index instrumentation"); + + VERIFY_ARE_NOT_EQUAL( + std::string::npos, + Disassemble(pOptimizedModule).find("dx.op.waveActiveAllEqual")); +} From 3a1095efd54f9e20c488b9378a0ed25b4b2401e1 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 18:54:08 -0700 Subject: [PATCH 06/39] [PIX] Record only real resource accesses, including samplers The shader access tracking pass records an access for each DXIL operation that takes a resource handle. Two of these operations are not accesses. annotateHandle attaches type information to a handle. barrierByMemoryHandle puts accesses in order. The pass therefore reports a read or a write at a point where the shader touches no memory. The pass also matches a library handle against the UAVs, the SRVs, and the constant buffers, but not against the samplers. A sampler access in a library shader gets no record, so the sampler binding looks unused. An access that consumes an annotated handle keeps its record. The pass still looks through the annotation to reach the resource. PIX receives fewer incorrect records and more sampler records. A tool that counts records will see different totals. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilShaderAccessTracking.cpp | 20 +++++ .../AccessTrackingBarrierIsNotAnAccess.hlsl | 27 +++++++ ...rackingLibAnnotateHandleIsNotAnAccess.hlsl | 30 ++++++++ ...ngLibAnnotatedHandleReadStillRecorded.hlsl | 33 ++++++++ tools/clang/unittests/HLSL/PixTest.cpp | 76 ++++++++++++++++++- 5 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index 1dddb6c0e6..8c5317d946 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -659,6 +659,17 @@ DxilResourceAndClass DxilShaderAccessTracking::DetermineAccessForHandleForLib( } } } + if (ret.registerType == RegisterType::Invalid) { + auto const &Samplers = DM.GetSamplers(); + for (auto &Sampler : Samplers) { + if (global == Sampler->GetGlobalSymbol()) { + binding = + hlsl::resource_helper::loadBindingFromResourceBase(Sampler.get()); + ret.registerType = RegisterType::Sampler; + break; + } + } + } if (ret.registerType != RegisterType::Invalid) { ret.accessStyle = AccessStyle::FromRootSig; ret.RegisterID = binding.rangeLowerBound; @@ -898,6 +909,15 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { // Special cases switch (opCode) { + case DXIL::OpCode::AnnotateHandle: + // annotateHandle attaches type information. It is not a resource + // access. GetResourceFromHandle still walks through it when a + // later access uses the annotated handle. + continue; + case DXIL::OpCode::BarrierByMemoryHandle: + // A barrier orders accesses to a resource. It is not itself an + // access. + continue; case DXIL::OpCode::GetDimensions: // readWrite = ShaderAccessFlags::DescriptorRead; // TODO: Support // GetDimensions diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl new file mode 100644 index 0000000000..73da834b3e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl @@ -0,0 +1,27 @@ +// RUN: %dxc -T cs_6_8 -E main -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=U0:0:2i0;.0;256;512. | %FileCheck %s + +// Barrier() on a resource handle orders accesses to that resource. It is +// not itself an access. +// +// The config puts the UAVs of space 0 at slot 0 onwards, so g_out is slot +// 0 and g_rw is slot 1. A slot is three dwords, so g_out's write dword is +// at byte 4 and g_rw's write dword is at byte 16. + +// g_rw is only barriered, never accessed, so nothing is recorded against +// it. +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +// The store to g_out is a genuine write and is recorded. +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 4, + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +RWByteAddressBuffer g_out : register(u0); +RWTexture2D g_rw : register(u1); + +[numthreads(1, 1, 1)] +void main(uint index : SV_GroupIndex) +{ + Barrier(g_rw, DEVICE_SCOPE); + g_out.Store(0, 1); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl new file mode 100644 index 0000000000..d81de35182 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl @@ -0,0 +1,30 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=S0:1:1i0;U0:2:1i0;.0;0;0. | %FileCheck %s + +// annotateHandle attaches type information to a handle. It is not a +// memory operation. g_untouched is only passed to GetDimensions, which +// this pass skips, so the annotation is that resource's only handle use. +// Nothing is recorded against it. +// +// The config puts the SRV of space 0 at slot 1 and the UAV of space 0 at +// slot 2. A slot is three dwords, so g_untouched's read dword is at byte +// 12 and its write dword at byte 16. g_output's write dword is at byte 28. + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 12, +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +// The store to g_output is a genuine access and is recorded: +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 28, + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 12, +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +Texture2D g_untouched : register(t0); +RWByteAddressBuffer g_output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + uint width, height; + g_untouched.GetDimensions(width, height); + g_output.Store(0, width + height); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl new file mode 100644 index 0000000000..b14588c0a4 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl @@ -0,0 +1,33 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=S0:1:1i0;U0:2:1i0;.256;512;1024. | %FileCheck %s + +// annotateHandle is not an access, but a genuine access that uses an +// annotated handle still records the resource class from that annotation. +// +// Offsets with this config (SRV space 0 at slot 1, UAV space 0 at slot 2, +// three dwords per slot, descriptor-heap records at byte 256): +// g_input read slot 1, read dword -> 12 +// g_input write slot 1, write dword -> 16 (must not appear) +// g_output write slot 2, write dword -> 28 +// heapTexture read descriptor 3 -> 292 +// +// A descriptor-heap record encodes shader kind in its top four bits and +// ResourceAccessStyle in the next four. RayGeneration is 7 and SRVRead is +// 5, so 0x75000000 == 1962934272. + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 12, +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 292, i32 undef, i32 1962934272, +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 28, +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +ByteAddressBuffer g_input : register(t0); +RWByteAddressBuffer g_output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + Texture2D heapTexture = ResourceDescriptorHeap[3]; + uint value = g_input.Load(0); + value += asuint(heapTexture.Load(int3(0, 0, 0)).x); + g_output.Store(0, value); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 239b6900be..b6a8cb918f 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -121,6 +121,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(AccessTracking_ModificationReport_Read) TEST_METHOD(AccessTracking_ModificationReport_Write) TEST_METHOD(AccessTracking_ModificationReport_SM66) + TEST_METHOD(AccessTracking_SamplerAccessInLibrary) TEST_METHOD(PixStructAnnotation_Lib_DualRaygen) @@ -185,6 +186,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(Validation_ControlInvalidModuleFails) TEST_METHOD(Validation_ControlBoilerplateOnlyFailureIsRejected) TEST_METHOD(Validation_NonUniformResourceIndex_WaveOpsFlag) + TEST_METHOD(Validation_ShaderAccessTracking_DynamicallyIndexedResource) dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -637,7 +639,8 @@ class PixTest : public ::testing::Test { const wchar_t *profile = L"as_6_5"); void ValidateAllocaWrite(std::vector const &allocaWrites, size_t index, const char *name); - PassOutput RunShaderAccessTrackingPass(IDxcBlob *blob); + PassOutput RunShaderAccessTrackingPass( + IDxcBlob *blob, const wchar_t *config = L"U0:0:10i0;U0:1:2i0;.0;0;0."); CComPtr RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob); CComPtr RunDxilPIXMeshShaderOutputPass(IDxcBlob *blob); @@ -921,14 +924,17 @@ TEST_F(PixTest, CompileDebugDisasmPDB) { VERIFY_SUCCEEDED(pCompiler->Disassemble(pPdbBlob, &pDisasm)); } -PassOutput PixTest::RunShaderAccessTrackingPass(IDxcBlob *blob) { +PassOutput PixTest::RunShaderAccessTrackingPass(IDxcBlob *blob, + const wchar_t *config) { CComPtr pOptimizer; VERIFY_SUCCEEDED( m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); std::vector Options; Options.push_back(L"-opt-mod-passes"); - Options.push_back(L"-hlsl-dxil-pix-shader-access-instrumentation,config=U0:0:" - L"10i0;U0:1:2i0;.0;0;0."); + std::wstring passOption = + L"-hlsl-dxil-pix-shader-access-instrumentation,config="; + passOption += config; + Options.push_back(passOption.c_str()); CComPtr pOptimizedModule; CComPtr pText; @@ -1286,6 +1292,47 @@ float main() : SV_Target ValidateAccessTrackingMods(hlsl, true); } +std::vector Split(std::string str, char delimeter); + +static bool HasBufferStoreWithByteOffset(std::vector const &lines, + unsigned byteOffset) { + std::string needle = "i32 " + std::to_string(byteOffset); + for (auto const &line : lines) { + if (line.find("dx.op.bufferStore") != std::string::npos && + line.find(needle) != std::string::npos) { + return true; + } + } + return false; +} + +TEST_F(PixTest, AccessTracking_SamplerAccessInLibrary) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +Texture2D g_texture : register(t0); +SamplerState g_sampler : register(s2); +RWByteAddressBuffer g_output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + float4 value = g_texture.SampleLevel(g_sampler, float2(0, 0), 0); + g_output.Store(0, asuint(value.x)); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"lib_6_6", {L"-Od"}); + auto output = RunShaderAccessTrackingPass( + compiled, L"S0:0:4i0;M0:20:4i0;U0:40:4i0;.0;0;0."); + auto lines = Split(Disassemble(output.blob), '\n'); + VERIFY_IS_TRUE(HasBufferStoreWithByteOffset(lines, 264)); + VerifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of a library sampler access"); +} + TEST_F(PixTest, AddToASGroupSharedPayload) { const char *hlsl = R"( @@ -4394,3 +4441,24 @@ float4 main(float4 pos : SV_Position) : SV_Target std::string::npos, Disassemble(pOptimizedModule).find("dx.op.waveActiveAllEqual")); } + +TEST_F(PixTest, Validation_ShaderAccessTracking_DynamicallyIndexedResource) { + const char *source = R"x( +Texture2D textures[8] : register(t0); +SamplerState samp : register(s0); + +cbuffer Constants : register(b0) +{ + uint index; +}; + +float4 main(float4 pos : SV_Position) : SV_Target +{ + return textures[index].Sample(samp, pos.xy); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunShaderAccessTrackingPass(compiled); + VerifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of a dynamically indexed resource"); +} From 60e6902b39d43619cbbd85fff4ae949c5a138114 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 18:54:24 -0700 Subject: [PATCH 07/39] [PIX] Fix dynamic bind-point identity and range bounds RSRegisterIdentifier::operator< combines its three fields with &&. This is not a strict weak ordering. Two distinct ranges can compare as neither less than the other, so the map treats them as one key and gives their slot assignments to one of them. Accesses to one range are then reported against the other. RegisterID comes from the position of the resource in the module's resource list. PIX needs the register that the binding occupies. The range check for a constant index is off by one, so the pass accepts an index that addresses the slot after the range. An index outside the range is still recorded at slot zero. The meaning of RegisterID changes. A tool that resolved the old value against the module's resource list must resolve the new value against the root signature. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilShaderAccessTracking.cpp | 12 ++- tools/clang/unittests/HLSL/PixTest.cpp | 83 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index 8c5317d946..f6ce712b5d 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -174,8 +174,9 @@ struct RSRegisterIdentifier { unsigned Index; bool operator<(const RSRegisterIdentifier &o) const { - return static_cast(Type) < static_cast(o.Type) && - Space < o.Space && Index < o.Index; + return static_cast(Type) < static_cast(o.Type) || + (Type == o.Type && + (Space < o.Space || (Space == o.Space && Index < o.Index))); } }; @@ -428,7 +429,8 @@ bool DxilShaderAccessTracking::EmitResourceAccess(DxilModule &DM, if (isa(res.index) && res.indexDynamicOffset == nullptr) { unsigned index = cast(res.index)->getLimitedValue(); - if (index > slot->second.numSlots) { + // Index is 0-based, so numSlots is the first out-of-range value. + if (index >= slot->second.numSlots) { // out-of-range accesses are written to slot zero: slotIndex = HlslOP->GetU32Const(0); } else { @@ -746,7 +748,8 @@ DxilShaderAccessTracking::GetResourceFromHandle(Value *resHandle, ret.index = createHandle.get_index(); ret.registerType = registerType; ret.accessStyle = AccessStyle::FromRootSig; - ret.RegisterID = resource->GetID(); + // RegisterID is the binding lower bound, not the resource-list ID. + ret.RegisterID = resource->GetLowerBound(); ret.RegisterSpace = resource->GetSpaceID(); } } @@ -768,6 +771,7 @@ DxilShaderAccessTracking::GetResourceFromHandle(Value *resHandle, ret.index = createHandleFromBinding.get_index(); ret.registerType = RegisterTypeFromResourceClass( static_cast(binding.resourceClass)); + ret.RegisterID = binding.rangeLowerBound; ret.RegisterSpace = binding.spaceID; } else if (hlsl::OP::IsDxilOpFuncCallInst( handleCreation, hlsl::OP::OpCode::CreateHandleFromHeap)) { diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index b6a8cb918f..57be05ff93 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -121,6 +121,9 @@ class PixTest : public ::testing::Test { TEST_METHOD(AccessTracking_ModificationReport_Read) TEST_METHOD(AccessTracking_ModificationReport_Write) TEST_METHOD(AccessTracking_ModificationReport_SM66) + TEST_METHOD(AccessTracking_MultipleDynamicRangesSameTypeAndSpace) + TEST_METHOD(AccessTracking_DynamicRangeRegisterIndex_SM66) + TEST_METHOD(AccessTracking_ConstantIndexAtRangeLimit) TEST_METHOD(AccessTracking_SamplerAccessInLibrary) TEST_METHOD(PixStructAnnotation_Lib_DualRaygen) @@ -1294,6 +1297,15 @@ float main() : SV_Target std::vector Split(std::string str, char delimeter); +static std::string JoinLines(std::vector const &lines) { + std::string joined; + for (auto const &line : lines) { + joined += line; + joined += '\n'; + } + return joined; +} + static bool HasBufferStoreWithByteOffset(std::vector const &lines, unsigned byteOffset) { std::string needle = "i32 " + std::to_string(byteOffset); @@ -1306,6 +1318,77 @@ static bool HasBufferStoreWithByteOffset(std::vector const &lines, return false; } +TEST_F(PixTest, AccessTracking_MultipleDynamicRangesSameTypeAndSpace) { + const char *hlsl = R"( +ByteAddressBuffer g_indices : register(t0); +RWByteAddressBuffer g_firstRange[2] : register(u4); +RWByteAddressBuffer g_secondRange[2] : register(u6); + +[numthreads(1, 1, 1)] +void CSMain() +{ + uint index = g_indices.Load(0); + g_firstRange[index].Store(0, 1); + g_secondRange[index].Store(0, 2); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"cs_6_0", {L"-Od"}, L"CSMain"); + auto output = + RunShaderAccessTrackingPass(compiled, L"S0:0:2i0;U0:0:10i0;.0;0;0."); + auto text = JoinLines(output.lines); + VERIFY_IS_TRUE(text.find("U0:4;") != std::string::npos); + VERIFY_IS_TRUE(text.find("U0:6;") != std::string::npos); + VerifyInstrumentedModuleIsValid(output.blob, + "shader access tracking of two dynamic UAV " + "ranges in the same register space"); +} + +TEST_F(PixTest, AccessTracking_DynamicRangeRegisterIndex_SM66) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +RWByteAddressBuffer g_buffers[] : register(u5); + +[numthreads(1, 1, 1)] +void CSMain(uint3 dispatchThreadId : SV_DispatchThreadID) +{ + g_buffers[dispatchThreadId.x].Store(0, 1); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"cs_6_6", {L"-Od"}, L"CSMain"); + auto output = RunShaderAccessTrackingPass(compiled, L"U0:0:10i0;.0;0;0."); + auto text = JoinLines(output.lines); + VERIFY_IS_TRUE(text.find("U0:5;") != std::string::npos); + VERIFY_IS_TRUE(text.find("U0:0;") == std::string::npos); + VerifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of an SM 6.6 dynamic UAV range"); +} + +TEST_F(PixTest, AccessTracking_ConstantIndexAtRangeLimit) { + const char *hlsl = R"( +RWByteAddressBuffer g_buffers[] : register(u0); + +[numthreads(1, 1, 1)] +void CSMain() +{ + g_buffers[1].Store(0, 1); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"cs_6_0", {L"-Od"}, L"CSMain"); + auto output = RunShaderAccessTrackingPass(compiled, L"U0:0:1i0;.0;0;0."); + auto lines = Split(Disassemble(output.blob), '\n'); + VERIFY_IS_TRUE(HasBufferStoreWithByteOffset(lines, 4)); + VERIFY_IS_TRUE(!HasBufferStoreWithByteOffset(lines, 16)); + VerifyInstrumentedModuleIsValid( + output.blob, + "shader access tracking of a constant index at the range limit"); +} + TEST_F(PixTest, AccessTracking_SamplerAccessInLibrary) { if (m_ver.SkipDxilVersion(1, 6)) { return; From 3b7c9fe39ad03c04484d37a7d4889617efa98817 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 18:54:25 -0700 Subject: [PATCH 08/39] [PIX] Attribute library accesses to reaching entry points The pass either skips a helper function in a library or gives its access the Library shader kind. Library is the kind of the module. It does not identify the pipeline stage that reaches the access. PIX cannot tell whether a record comes from a ray generation, closest-hit, or miss shader. The access record holds the instruction number in a field of 24 bits. A larger number overflows into the adjacent fields of the record. PIX receives records for helper functions, and each record carries a pipeline stage. A tool that filtered out records with the Library kind will see more data. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilShaderAccessTracking.cpp | 106 +++++++++++++++--- .../AccessTrackingLibHelperShaderKind.hlsl | 31 +++++ tools/clang/unittests/HLSL/PixTest.cpp | 82 ++++++++++++++ 3 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index f6ce712b5d..1f00e3c1bd 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -28,6 +28,8 @@ #include "llvm/Transforms/Utils/Local.h" #include +#include +#include #include "PixPassHelpers.h" @@ -546,11 +548,16 @@ bool DxilShaderAccessTracking::EmitResourceAccess(DxilModule &DM, Builder.CreateMul(ZeroIfOutOfBounds, EncodedFlags); uint32_t InstructionNumber = 0; (void)pix_dxil::PixDxilInstNum::FromInst(instruction, &InstructionNumber); - auto const *shaderModel = DM.GetShaderModel(); - auto shaderKind = shaderModel->GetKind(); - uint32_t EncodedInstructionNumber = InstructionNumber | - InstructionOrdinalndicator | - EncodeShaderModel(shaderKind); + auto *EncodedShaderKindConstant = cast( + m_FunctionToEncodedAccess.at(Builder.GetInsertBlock()->getParent()) + .at(ResourceAccessStyle::None)); + uint32_t EncodedShaderKind = EncodedShaderKindConstant->getLimitedValue(); + // The ordinal occupies the low 24 bits. Mask it so it does not overlap + // the indicator or the shader kind. + constexpr uint32_t InstructionOrdinalMask = 0x00FF'FFFF; + uint32_t EncodedInstructionNumber = + (InstructionNumber & InstructionOrdinalMask) | + InstructionOrdinalndicator | EncodedShaderKind; auto *MultipliedOutOfBoundsValue = Builder.CreateMul( OneIfOutOfBounds, HlslOP->GetU32Const(EncodedInstructionNumber)); auto *CombinedFlagOrInstructionValue = @@ -810,6 +817,69 @@ DxilShaderAccessTracking::GetResourceFromHandle(Value *resHandle, return ret; } +// Map each function to the shader kind of the entry point that reaches it. +// A library helper has no DxilFunctionProps, so the module kind is Library, +// which PIX cannot attribute to a pipeline stage. If more than one entry +// kind reaches the same helper, keep the module kind. Entry points keep +// their own kind. +static std::map +ResolveShaderKindByReachingEntryPoint(DxilModule &DM) { + std::map functionToShaderKind; + + const DXIL::ShaderKind ambiguousShaderKind = DM.GetShaderModel()->GetKind(); + auto entryPoints = DM.GetExportedFunctions(); + + for (llvm::Function *entryPoint : entryPoints) { + if (entryPoint == nullptr || entryPoint->isDeclaration()) { + continue; + } + + const DXIL::ShaderKind entryPointShaderKind = + PIXPassHelpers::GetFunctionShaderKind(DM, entryPoint); + + std::vector pending{entryPoint}; + std::set visited; + while (!pending.empty()) { + llvm::Function *reached = pending.back(); + pending.pop_back(); + if (!visited.insert(reached).second) { + continue; + } + + auto emplaced = + functionToShaderKind.emplace(reached, entryPointShaderKind); + if (!emplaced.second && emplaced.first->second != entryPointShaderKind) { + emplaced.first->second = ambiguousShaderKind; + } + + for (llvm::BasicBlock &block : reached->getBasicBlockList()) { + for (llvm::Instruction &instruction : block.getInstList()) { + auto *call = llvm::dyn_cast(&instruction); + if (call == nullptr) { + continue; + } + llvm::Function *callee = call->getCalledFunction(); + if (callee == nullptr || callee->isDeclaration() || + callee->isIntrinsic() || hlsl::OP::IsDxilOpFunc(callee)) { + continue; + } + pending.push_back(callee); + } + } + } + } + + for (llvm::Function *entryPoint : entryPoints) { + if (entryPoint == nullptr || entryPoint->isDeclaration()) { + continue; + } + functionToShaderKind[entryPoint] = + PIXPassHelpers::GetFunctionShaderKind(DM, entryPoint); + } + + return functionToShaderKind; +} + bool DxilShaderAccessTracking::runOnModule(Module &M) { // This pass adds instrumentation for shader access to resources @@ -840,6 +910,8 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { auto instrumentableFunctions = PIXPassHelpers::GetAllInstrumentableFunctions(DM); + auto functionToShaderKind = ResolveShaderKindByReachingEntryPoint(DM); + if (DM.m_ShaderFlags.GetForceEarlyDepthStencil()) { if (OSOverride != nullptr) { formatted_raw_ostream FOS(*OSOverride); @@ -851,17 +923,11 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { PIXPassHelpers::CreateGlobalUAVResource(DM, 0u, "PIX_ShaderAccessUAV"); for (auto *F : instrumentableFunctions) { - DXIL::ShaderKind shaderKind = DXIL::ShaderKind::Invalid; - if (!DM.HasDxilFunctionProps(F)) { - auto ShaderModel = DM.GetShaderModel(); - shaderKind = ShaderModel->GetKind(); - if (shaderKind == DXIL::ShaderKind::Library) { - continue; - } - } else { - hlsl::DxilFunctionProps const &props = DM.GetDxilFunctionProps(F); - shaderKind = props.shaderKind; - } + auto reachedFrom = functionToShaderKind.find(F); + DXIL::ShaderKind shaderKind = + reachedFrom != functionToShaderKind.end() + ? reachedFrom->second + : PIXPassHelpers::GetFunctionShaderKind(DM, F); IRBuilder<> Builder(F->getEntryBlock().getFirstInsertionPt()); @@ -959,9 +1025,13 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { } for (unsigned iParam : handleParams) { + auto uavHandle = + m_FunctionToUAVHandle.find(CallerParent->getParent()); + if (uavHandle == m_FunctionToUAVHandle.end()) + continue; + // Don't instrument the accesses to the UAV that we just added - if (Call->getArgOperand(iParam) == - m_FunctionToUAVHandle[CallerParent->getParent()]) + if (Call->getArgOperand(iParam) == uavHandle->second) continue; auto res = GetResourceFromHandle(Call->getArgOperand(iParam), DM); if (res.accessStyle == AccessStyle::None) { diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl new file mode 100644 index 0000000000..a998d42218 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl @@ -0,0 +1,31 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=.256;512;1024. | %FileCheck %s + +// A descriptor-heap access record carries the shader kind of the entry +// point that reached the access. HeapHelper is [noinline] so the access +// stays in the helper. The descriptor index is a parameter so the helper +// is not folded away. +// +// The kind occupies bits 31:28. An out-of-bounds record sets the +// instruction-ordinal indicator (bit 27). RayGeneration is 7 and UAVWrite +// is 3, so the in-bounds flags are 0x73000000 == 1929379840 and the +// out-of-bounds value is 0x78000000 == 2013265920. Under the module kind, +// Library (6), those values would be 0x63000000 and 0x68000000. + +// CHECK: define void {{.*}}HeapHelper +// CHECK-NOT: 1660944384 +// CHECK: mul i32 {{.*}}, 1929379840 +// CHECK-NOT: 1744830464 +// CHECK: mul i32 {{.*}}, 2013265920 + +[noinline] +export void HeapHelper(uint descriptorIndex) +{ + RWByteAddressBuffer heapBuffer = ResourceDescriptorHeap[descriptorIndex]; + heapBuffer.Store(0, 1); +} + +[shader("raygeneration")] +void RayGen() +{ + HeapHelper(1); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 57be05ff93..0fa134c98f 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -125,6 +125,8 @@ class PixTest : public ::testing::Test { TEST_METHOD(AccessTracking_DynamicRangeRegisterIndex_SM66) TEST_METHOD(AccessTracking_ConstantIndexAtRangeLimit) TEST_METHOD(AccessTracking_SamplerAccessInLibrary) + TEST_METHOD(AccessTracking_OobBindlessUsesFunctionShaderKind) + TEST_METHOD(AccessTracking_LibraryNonEntryFunction) TEST_METHOD(PixStructAnnotation_Lib_DualRaygen) @@ -1318,6 +1320,28 @@ static bool HasBufferStoreWithByteOffset(std::vector const &lines, return false; } +static bool +HasBufferStoreValueMatchingMask(std::vector const &lines, + uint32_t mask, uint32_t maskedValue) { + for (auto const &line : lines) { + if (line.find("dx.op.bufferStore") == std::string::npos) { + continue; + } + + size_t position = 0; + while ((position = line.find("i32 ", position)) != std::string::npos) { + position += 4; + char *end = nullptr; + uint32_t value = + static_cast(strtoul(line.c_str() + position, &end, 10)); + if (end != line.c_str() + position && (value & mask) == maskedValue) { + return true; + } + } + } + return false; +} + TEST_F(PixTest, AccessTracking_MultipleDynamicRangesSameTypeAndSpace) { const char *hlsl = R"( ByteAddressBuffer g_indices : register(t0); @@ -1416,6 +1440,64 @@ void RayGen() output.blob, "shader access tracking of a library sampler access"); } +TEST_F(PixTest, AccessTracking_OobBindlessUsesFunctionShaderKind) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +[shader("raygeneration")] +void RayGen() +{ + RWByteAddressBuffer output = ResourceDescriptorHeap[1]; + output.Store(0, 1); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"lib_6_6", {L"-Od"}); + auto output = RunShaderAccessTrackingPass(compiled, L".0;0;0."); + auto lines = Split(Disassemble(output.blob), '\n'); + VERIFY_IS_TRUE( + HasBufferStoreValueMatchingMask(lines, 0xF8000000, 0x78000000)); + VERIFY_IS_TRUE( + !HasBufferStoreValueMatchingMask(lines, 0xF8000000, 0x68000000)); + VerifyInstrumentedModuleIsValid( + output.blob, + "shader access tracking of an out-of-bounds bindless access"); +} + +TEST_F(PixTest, AccessTracking_LibraryNonEntryFunction) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +Texture2D g_texture : register(t0); +RWByteAddressBuffer g_output : register(u0); + +export float4 Helper(uint index) +{ + float4 value = g_texture.Load(int3(index, 0, 0)); + g_output.Store(0, asuint(value.x)); + return value; +} + +[shader("raygeneration")] +void RayGen() +{ + Helper(0); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"lib_6_6", {L"-Od"}); + auto output = + RunShaderAccessTrackingPass(compiled, L"S0:0:4i0;U0:4:4i0;.0;0;0."); + auto text = JoinLines(output.lines); + VERIFY_IS_TRUE(text.find("NotModified") == std::string::npos); + VerifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of a library helper function"); +} + TEST_F(PixTest, AddToASGroupSharedPayload) { const char *hlsl = R"( From b8ea3e51ef4930bcdf586c724ca77b3227a17929 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 24 Aug 2026 16:51:36 -0700 Subject: [PATCH 09/39] [PIX] Fix render-state visualization passes dx.op.storeOutput has four legal overloads: f16, f32, i16, and i32. DxilOutputColorBecomesConstant looks only for f32 and i32. A shader that writes SV_Target0 as min16float, min16int, half, or int16_t gets no constant-color instrumentation. The pass then asserts that the outputs are not mixed, and that assertion cannot tell "both types present" from "neither type recognised". The pass builds a constant buffer for the colour, but it does not give that buffer a layout ValidateCBuffer can reach. The validator therefore refuses the instrumented module. DxilReduceMSAAToSingleSample does not look through annotateHandle, and it does not accept a 16-bit texture load. Only one overload can write SV_Target0, so there is no mixed case to resolve. PIX always uploads four 32-bit components, so the shader narrows them when the output is 16 bits. The upload format does not change. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilOutputColorBecomesConstant.cpp | 193 +++++++++++------- .../DxilReduceMSAAToSingleSample.cpp | 95 +++++---- .../HLSLFileCheck/pix/constantcolorhalf.hlsl | 19 ++ .../pix/constantcolorhalfFromCB.hlsl | 36 ++++ .../pix/constantcolorhalfMRT.hlsl | 29 +++ .../pix/constantcolorhalfMRTOnRTV1.hlsl | 33 +++ .../HLSLFileCheck/pix/constantcolorint16.hlsl | 18 ++ .../pix/constantcolorminprecision.hlsl | 18 ++ .../pix/constantcolorminprecisionFromCB.hlsl | 37 ++++ .../pix/constantcolorminprecisionMRT.hlsl | 29 +++ .../pix/constantcolorminprecisionint.hlsl | 18 ++ tools/clang/unittests/HLSL/PixTest.cpp | 118 +++++++++++ 12 files changed, 531 insertions(+), 112 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl diff --git a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp index 227cc890a7..4eb9ba281e 100644 --- a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp +++ b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp @@ -12,6 +12,7 @@ #include "dxc/DXIL/DxilModule.h" #include "dxc/DXIL/DxilOperations.h" +#include "dxc/DXIL/DxilTypeSystem.h" #include "dxc/DxilPIXPasses/DxilPIXPasses.h" #include "dxc/HLSL/DxilGenerationPass.h" #include "dxc/HLSL/DxilSpanAllocator.h" @@ -108,52 +109,78 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { const hlsl::DxilSignature &OutputSignature = DM.GetOutputSignature(); - Function *FloatOutputFunction = - HlslOP->GetOpFunc(DXIL::OpCode::StoreOutput, Type::getFloatTy(Ctx)); - Function *IntOutputFunction = - HlslOP->GetOpFunc(DXIL::OpCode::StoreOutput, Type::getInt32Ty(Ctx)); + // dx.op.storeOutput has four legal overloads: f16, f32, i16 and i32. + // A min16float or min16int SV_Target lowers through the f16 or i16 form, + // as does a native half or int16_t target under -enable-16bit-types. + const std::array OverloadTypes{ + Type::getHalfTy(Ctx), Type::getFloatTy(Ctx), Type::getInt16Ty(Ctx), + Type::getInt32Ty(Ctx)}; - bool hasFloatOutputs = false; - bool hasIntOutputs = false; + std::array OutputFunctions{}; + size_t ActiveOverload = OverloadTypes.size(); - visitOutputInstructionCallers( - FloatOutputFunction, OutputSignature, HlslOP, - [&hasFloatOutputs](CallInst *) { hasFloatOutputs = true; }); + for (size_t OverloadIndex = 0; OverloadIndex < OverloadTypes.size(); + ++OverloadIndex) { + OutputFunctions[OverloadIndex] = HlslOP->GetOpFunc( + DXIL::OpCode::StoreOutput, OverloadTypes[OverloadIndex]); - visitOutputInstructionCallers( - IntOutputFunction, OutputSignature, HlslOP, - [&hasIntOutputs](CallInst *) { hasIntOutputs = true; }); + bool HasTargetZeroStores = false; + visitOutputInstructionCallers( + OutputFunctions[OverloadIndex], OutputSignature, HlslOP, + [&HasTargetZeroStores](CallInst *) { HasTargetZeroStores = true; }); + + if (HasTargetZeroStores) { + // visitOutputInstructionCallers filters on SemanticKind::Target with + // GetSemanticStartIndex() == 0, so at most one overload writes + // SV_Target0. + DXASSERT(ActiveOverload == OverloadTypes.size(), + "Only one storeOutput overload can write SV_Target0"); + ActiveOverload = OverloadIndex; + } + } + + // GetOpFunc materialises each overload declaration on demand. Any + // overload with no callers must be erased before the pass returns; the + // validator rejects a module carrying an unused dx.op declaration. + struct EraseUnusedOutputFunctionsOnExit { + hlsl::DxilModule &DM; + std::array &OutputFunctions; + ~EraseUnusedOutputFunctionsOnExit() { + for (Function *OutputFunction : OutputFunctions) { + PIXPassHelpers::EraseIfUnused(DM, OutputFunction); + } + } + } EraseUnusedOutputFunctions{DM, OutputFunctions}; - if (!hasFloatOutputs && !hasIntOutputs) { - PIXPassHelpers::EraseIfUnused(DM, FloatOutputFunction); - PIXPassHelpers::EraseIfUnused(DM, IntOutputFunction); + if (ActiveOverload == OverloadTypes.size()) { return false; } - // Otherwise, we assume the shader outputs only one or the other (because the - // 0th RTV can't have a mixed type) - DXASSERT(!hasFloatOutputs || !hasIntOutputs, - "Only one or the other type of output: float or int"); + // Replacement values must match the store's own overload type. + llvm::Type *const OutputValueType = OverloadTypes[ActiveOverload]; + const bool IsFloatOutput = OutputValueType->isFloatingPointTy(); std::array ReplacementColors; switch (Mode) { case FromLiteralConstant: { - if (hasFloatOutputs) { - ReplacementColors[0] = HlslOP->GetFloatConst(Red); - ReplacementColors[1] = HlslOP->GetFloatConst(Green); - ReplacementColors[2] = HlslOP->GetFloatConst(Blue); - ReplacementColors[3] = HlslOP->GetFloatConst(Alpha); - } - if (hasIntOutputs) { - ReplacementColors[0] = HlslOP->GetI32Const(static_cast(Red)); - ReplacementColors[1] = HlslOP->GetI32Const(static_cast(Green)); - ReplacementColors[2] = HlslOP->GetI32Const(static_cast(Blue)); - ReplacementColors[3] = HlslOP->GetI32Const(static_cast(Alpha)); + const std::array Channels{Red, Green, Blue, Alpha}; + for (size_t ChannelIndex = 0; ChannelIndex < Channels.size(); + ++ChannelIndex) { + ReplacementColors[ChannelIndex] = + IsFloatOutput + ? ConstantFP::get(OutputValueType, Channels[ChannelIndex]) + : ConstantInt::get(OutputValueType, + static_cast(static_cast( + Channels[ChannelIndex])), + /*isSigned*/ true); } } break; case FromConstantBuffer: { + // A float4 constant buffer row is 16 bytes wide. + constexpr unsigned int ConstantColorCBufferSizeInBytes = 4 * sizeof(float); + // Setup a constant buffer with a single float4 in it: SmallVector Elements{ Type::getFloatTy(Ctx), Type::getFloatTy(Ctx), Type::getFloatTy(Ctx), @@ -162,13 +189,31 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { llvm::StructType::create(Elements, "PIX_ConstantColorCB_Type"); std::unique_ptr pCBuf = llvm::make_unique(); pCBuf->SetGlobalName("PIX_ConstantColorCBName"); - pCBuf->SetGlobalSymbol(UndefValue::get(CBStructTy)); + // The global symbol and HLSL type must be pointers to the struct so + // ValidateCBuffer can reach the annotation. + pCBuf->SetGlobalSymbol(UndefValue::get(CBStructTy->getPointerTo())); + pCBuf->SetHLSLType(CBStructTy->getPointerTo()); pCBuf->SetID(static_cast(DM.GetCBuffers().size())); pCBuf->SetSpaceID( (unsigned int)-2); // This is the reserved-for-tools register space pCBuf->SetLowerBound(0); pCBuf->SetRangeSize(1); - pCBuf->SetSize(4); + pCBuf->SetSize(ConstantColorCBufferSizeInBytes); + + auto *StructAnnotation = DM.GetTypeSystem().GetStructAnnotation(CBStructTy); + if (StructAnnotation == nullptr) { + StructAnnotation = DM.GetTypeSystem().AddStructAnnotation(CBStructTy); + StructAnnotation->SetCBufferSize(ConstantColorCBufferSizeInBytes); + static const char *const ComponentNames[] = {"r", "g", "b", "a"}; + for (unsigned int ComponentIndex = 0; ComponentIndex < 4; + ++ComponentIndex) { + auto &FieldAnnotation = + StructAnnotation->GetFieldAnnotation(ComponentIndex); + FieldAnnotation.SetCBufferOffset(ComponentIndex * sizeof(float)); + FieldAnnotation.SetCompType(hlsl::DXIL::ComponentType::F32); + FieldAnnotation.SetFieldName(ComponentNames[ComponentIndex]); + } + } Instruction *entryPointInstruction = &*(PIXPassHelpers::GetEntryFunction(DM)->begin()->begin()); @@ -188,9 +233,12 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { #define PIX_CONSTANT_VALUE "PIX_Constant_Color_Value" // Insert the Buffer load instruction: - Function *CBLoad = HlslOP->GetOpFunc( - OP::OpCode::CBufferLoadLegacy, - hasFloatOutputs ? Type::getFloatTy(Ctx) : Type::getInt32Ty(Ctx)); + // The tools constant buffer is always four 32-bit components; PIX + // uploads that layout. + llvm::Type *const CBufferComponentType = + IsFloatOutput ? Type::getFloatTy(Ctx) : Type::getInt32Ty(Ctx); + Function *CBLoad = + HlslOP->GetOpFunc(OP::OpCode::CBufferLoadLegacy, CBufferComponentType); Constant *OpArg = HlslOP->GetU32Const((unsigned)OP::OpCode::CBufferLoadLegacy); Value *ResourceHandle = callCreateHandle; @@ -207,54 +255,47 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { Builder.CreateExtractValue(loadLegacy, 2, PIX_CONSTANT_VALUE "2"); ReplacementColors[3] = Builder.CreateExtractValue(loadLegacy, 3, PIX_CONSTANT_VALUE "3"); + + // Narrow the loaded components to a 16-bit output overload. + if (OutputValueType != CBufferComponentType) { + static const char *const NarrowedNames[] = { + PIX_CONSTANT_VALUE "Narrowed0", PIX_CONSTANT_VALUE "Narrowed1", + PIX_CONSTANT_VALUE "Narrowed2", PIX_CONSTANT_VALUE "Narrowed3"}; + for (size_t ChannelIndex = 0; ChannelIndex < ReplacementColors.size(); + ++ChannelIndex) { + ReplacementColors[ChannelIndex] = + IsFloatOutput + ? Builder.CreateFPTrunc(ReplacementColors[ChannelIndex], + OutputValueType, + NarrowedNames[ChannelIndex]) + : Builder.CreateTrunc(ReplacementColors[ChannelIndex], + OutputValueType, + NarrowedNames[ChannelIndex]); + } + } } break; default: assert(false); - return 0; + return false; } bool Modified = false; - // The StoreOutput function can store either a float or an integer, depending - // on the intended output render-target resource view. - if (hasFloatOutputs) { - visitOutputInstructionCallers( - FloatOutputFunction, OutputSignature, HlslOP, - [&ReplacementColors, &Modified](CallInst *CallInstruction) { - Modified = true; - // The output column is the channel (red, green, blue or alpha) within - // the output pixel - Value *OutputColumnOperand = CallInstruction->getOperand( - hlsl::DXIL::OperandIndex::kStoreOutputColOpIdx); - ConstantInt *OutputColumnConstant = - cast(OutputColumnOperand); - APInt OutputColumn = OutputColumnConstant->getValue(); - CallInstruction->setOperand( - hlsl::DXIL::OperandIndex::kStoreOutputValOpIdx, - ReplacementColors[*OutputColumn.getRawData()]); - }); - } - - if (hasIntOutputs) { - visitOutputInstructionCallers( - IntOutputFunction, OutputSignature, HlslOP, - [&ReplacementColors, &Modified](CallInst *CallInstruction) { - Modified = true; - // The output column is the channel (red, green, blue or alpha) within - // the output pixel - Value *OutputColumnOperand = CallInstruction->getOperand( - hlsl::DXIL::OperandIndex::kStoreOutputColOpIdx); - ConstantInt *OutputColumnConstant = - cast(OutputColumnOperand); - APInt OutputColumn = OutputColumnConstant->getValue(); - CallInstruction->setOperand( - hlsl::DXIL::OperandIndex::kStoreOutputValOpIdx, - ReplacementColors[*OutputColumn.getRawData()]); - }); - } - - PIXPassHelpers::EraseIfUnused(DM, FloatOutputFunction); - PIXPassHelpers::EraseIfUnused(DM, IntOutputFunction); + visitOutputInstructionCallers( + OutputFunctions[ActiveOverload], OutputSignature, HlslOP, + [&ReplacementColors, &Modified](CallInst *CallInstruction) { + Modified = true; + // The output column is the channel (red, green, blue or alpha) within + // the output pixel + Value *OutputColumnOperand = CallInstruction->getOperand( + hlsl::DXIL::OperandIndex::kStoreOutputColOpIdx); + ConstantInt *OutputColumnConstant = + cast(OutputColumnOperand); + APInt OutputColumn = OutputColumnConstant->getValue(); + CallInstruction->setOperand( + hlsl::DXIL::OperandIndex::kStoreOutputValOpIdx, + ReplacementColors[*OutputColumn.getRawData()]); + }); return Modified; } diff --git a/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp b/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp index 01f06605a5..9cb685c647 100644 --- a/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp +++ b/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp @@ -13,6 +13,7 @@ #include "dxc/DXIL/DxilInstructions.h" #include "dxc/DXIL/DxilModule.h" +#include "dxc/DXIL/DxilResourceProperties.h" #include "dxc/DxilPIXPasses/DxilPIXPasses.h" #include "dxc/HLSL/DxilGenerationPass.h" @@ -34,50 +35,72 @@ class DxilReduceMSAAToSingleSample : public ModulePass { bool runOnModule(Module &M) override; }; -bool DxilReduceMSAAToSingleSample::runOnModule(Module &M) { - DxilModule &DM = M.GetOrCreateDxilModule(); +static bool IsMultisampledSRVHandle(Value *TextureHandle, DxilModule &DM) { + auto *TextureHandleInst = dyn_cast(TextureHandle); + if (!TextureHandleInst) + return false; + + if (OP::IsDxilOpFuncCallInst(TextureHandleInst, OP::OpCode::CreateHandle)) { + DxilInst_CreateHandle CreateHandle(TextureHandleInst); + if (!isa(CreateHandle.get_rangeId())) + return false; + + if (static_cast( + CreateHandle.get_resourceClass_val()) != DXIL::ResourceClass::SRV) + return false; + + unsigned RangeId = + cast(CreateHandle.get_rangeId())->getLimitedValue(); + auto Resource = DM.GetSRV(RangeId); + return Resource.GetKind() == DXIL::ResourceKind::Texture2DMS || + Resource.GetKind() == DXIL::ResourceKind::Texture2DMSArray; + } - LLVMContext &Ctx = M.getContext(); - OP *HlslOP = DM.GetOP(); + // SM 6.6 handles carry the resource kind in the annotateHandle + // properties operand. + if (OP::IsDxilOpFuncCallInst(TextureHandleInst, OP::OpCode::AnnotateHandle)) { + DxilInst_AnnotateHandle AnnotateHandle(TextureHandleInst); + DxilResourceProperties ResourceProperties = + resource_helper::loadPropsFromAnnotateHandle(AnnotateHandle, + *DM.GetShaderModel()); + return ResourceProperties.getResourceClass() == DXIL::ResourceClass::SRV && + (ResourceProperties.getResourceKind() == + DXIL::ResourceKind::Texture2DMS || + ResourceProperties.getResourceKind() == + DXIL::ResourceKind::Texture2DMSArray); + } - // FP16 type doesn't have its own identity, and is covered by float type... + return false; +} - auto TextureLoadOverloads = std::vector{ - Type::getFloatTy(Ctx), Type::getInt16Ty(Ctx), Type::getInt32Ty(Ctx)}; +bool DxilReduceMSAAToSingleSample::runOnModule(Module &M) { + DxilModule &DM = M.GetOrCreateDxilModule(); + OP *HlslOP = DM.GetOP(); bool Modified = false; - for (const auto &Overload : TextureLoadOverloads) { + // Iterate every materialised TextureLoad overload; the 16-bit form + // lowers Texture2DMS.Load. + for (const auto &TextureLoadOverload : + HlslOP->GetOpFuncList(DXIL::OpCode::TextureLoad)) { + Function *TexLoadFunction = TextureLoadOverload.second; + if (!TexLoadFunction) + continue; - Function *TexLoadFunction = - HlslOP->GetOpFunc(DXIL::OpCode::TextureLoad, Overload); - auto TexLoadFunctionUses = TexLoadFunction->uses(); - - for (auto FI = TexLoadFunctionUses.begin(); - FI != TexLoadFunctionUses.end();) { + for (auto FI = TexLoadFunction->use_begin(); + FI != TexLoadFunction->use_end();) { auto &FunctionUse = *FI++; - auto FunctionUser = FunctionUse.getUser(); - auto instruction = cast(FunctionUser); - DxilInst_TextureLoad LoadInstruction(instruction); - auto TextureHandle = LoadInstruction.get_srv(); - auto TextureHandleInst = cast(TextureHandle); - DxilInst_CreateHandle createHandle(TextureHandleInst); - // Dynamic rangeId is not supported - if (isa(createHandle.get_rangeId())) { - unsigned rangeId = - cast(createHandle.get_rangeId())->getLimitedValue(); - if (static_cast( - createHandle.get_resourceClass_val()) == - DXIL::ResourceClass::SRV) { - auto Resource = DM.GetSRV(rangeId); - if (Resource.GetKind() == DXIL::ResourceKind::Texture2DMS || - Resource.GetKind() == DXIL::ResourceKind::Texture2DMSArray) { - // "2" is the mip-level/sample-index operand index: - // https://github.com/Microsoft/DirectXShaderCompiler/blob/master/docs/DXIL.rst#textureload - instruction->setOperand(2, HlslOP->GetI32Const(0)); - Modified = true; - } - } + auto *InstructionUser = dyn_cast(FunctionUse.getUser()); + if (!InstructionUser) + continue; + + DxilInst_TextureLoad LoadInstruction(InstructionUser); + if (!LoadInstruction) + continue; + + if (IsMultisampledSRVHandle(LoadInstruction.get_srv(), DM)) { + LoadInstruction.set_mipLevelOrSampleCount(HlslOP->GetI32Const(0)); + Modified = true; } } } diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl new file mode 100644 index 0000000000..e7129392ca --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl @@ -0,0 +1,19 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor,constant-red=0.5,constant-green=0.25,constant-blue=0.125,constant-alpha=1 | %FileCheck %s + +// A native half SV_Target lowers to dx.op.storeOutput.f16. + +// The override values are 0.5, 0.25, 0.125 and 1.0 as half: +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3800) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3400) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// Unused storeOutput overloads must not remain as external declarations. +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i16 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +half4 main() : SV_Target { + return half4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl new file mode 100644 index 0000000000..5638a098ee --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl @@ -0,0 +1,36 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor,mod-mode=1 | %FileCheck %s + +// From-constant-buffer mode against a native half SV_Target0. The tools +// constant buffer is four 32-bit components; loaded values narrow to half. + +// CB return type is f32: +// CHECK: %dx.types.CBufRet.f32 = type { float, float, float, float } + +// Create handle: +// CHECK: %PIX_Constant_Color_CB_Handle = call %dx.types.Handle @dx.op.createHandle(i32 57, i8 2, i32 0, i32 0, i1 false) + +// Load the row: +// CHECK: %PIX_Constant_Color_Value = call %dx.types.CBufRet.f32 @dx.op.cbufferLoadLegacy.f32(i32 59, %dx.types.Handle %PIX_Constant_Color_CB_Handle, i32 0) + +// Extract components: +// CHECK: %PIX_Constant_Color_Value0 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 0 +// CHECK: %PIX_Constant_Color_Value1 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 1 +// CHECK: %PIX_Constant_Color_Value2 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 2 +// CHECK: %PIX_Constant_Color_Value3 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 3 + +// Narrow to half: +// CHECK: %PIX_Constant_Color_ValueNarrowed0 = fptrunc float %PIX_Constant_Color_Value0 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed1 = fptrunc float %PIX_Constant_Color_Value1 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed2 = fptrunc float %PIX_Constant_Color_Value2 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed3 = fptrunc float %PIX_Constant_Color_Value3 to half + +// Store SV_Target0: +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half %PIX_Constant_Color_ValueNarrowed0) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half %PIX_Constant_Color_ValueNarrowed1) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half %PIX_Constant_Color_ValueNarrowed2) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half %PIX_Constant_Color_ValueNarrowed3) + +[RootSignature("")] +half4 main() : SV_Target { + return half4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl new file mode 100644 index 0000000000..5d4462c463 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl @@ -0,0 +1,29 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor | %FileCheck %s + +// MRT: RTV0 is half4, RTV1 is float4. The override applies to SV_Target0 +// only. Default constant colour is 1.0 (0xH3C00 as half). + +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// RTV1 stays unchanged: +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 0, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 1, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 2, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 3, float 0.000000e+00) + +struct RTOut +{ + half4 h : SV_Target; + float4 c : SV_Target1; +}; + +[RootSignature("")] +RTOut main() { + RTOut rtOut; + rtOut.h = half4(0, 0, 0, 0); + rtOut.c = float4(0.f, 0.f, 0.f, 0.f); + return rtOut; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl new file mode 100644 index 0000000000..f8e625001e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl @@ -0,0 +1,33 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor | %FileCheck %s + +// MRT: RTV0 is float4, RTV1 is half4. The override applies to SV_Target0 +// only; RTV1 stays unchanged. + +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 0, float 1.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 1, float 1.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 2, float 1.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 3, float 1.000000e+00) + +// RTV1 stays 0xH0000 (half 0.0): +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 0, half 0xH0000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 1, half 0xH0000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 2, half 0xH0000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 3, half 0xH0000) + +// Unused integer overloads must not remain as external declarations. +// CHECK-NOT: declare void @dx.op.storeOutput.i16 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +struct RTOut +{ + float4 c : SV_Target; + half4 h : SV_Target1; +}; + +[RootSignature("")] +RTOut main() { + RTOut rtOut; + rtOut.c = float4(0.f, 0.f, 0.f, 0.f); + rtOut.h = half4(0, 0, 0, 0); + return rtOut; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl new file mode 100644 index 0000000000..835fc3f013 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor,constant-red=8,constant-green=7,constant-blue=6,constant-alpha=5 | %FileCheck %s + +// A native uint16_t SV_Target lowers to dx.op.storeOutput.i16. + +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 0, i16 8) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 1, i16 7) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 2, i16 6) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 3, i16 5) + +// Unused storeOutput overloads must not remain as external declarations. +// CHECK-NOT: declare void @dx.op.storeOutput.f16 +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +uint16_t4 main() : SV_Target { + return uint16_t4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl new file mode 100644 index 0000000000..08d0fcae82 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor,constant-red=0.5,constant-green=0.25,constant-blue=0.125,constant-alpha=1 | %FileCheck %s + +// A min16float SV_Target lowers to dx.op.storeOutput.f16 at ps_6_0 without +// -enable-16bit-types. + +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3800) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3400) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i16 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +min16float4 main() : SV_Target { + return min16float4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl new file mode 100644 index 0000000000..1a15bdcc59 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor,mod-mode=1 | %FileCheck %s + +// From-constant-buffer mode against a min16float SV_Target0 at ps_6_0 +// without -enable-16bit-types. The tools constant buffer is four 32-bit +// components; loaded values narrow to half. + +// CB return type is f32: +// CHECK: %dx.types.CBufRet.f32 = type { float, float, float, float } + +// Create handle: +// CHECK: %PIX_Constant_Color_CB_Handle = call %dx.types.Handle @dx.op.createHandle(i32 57, i8 2, i32 0, i32 0, i1 false) + +// Load the row: +// CHECK: %PIX_Constant_Color_Value = call %dx.types.CBufRet.f32 @dx.op.cbufferLoadLegacy.f32(i32 59, %dx.types.Handle %PIX_Constant_Color_CB_Handle, i32 0) + +// Extract components: +// CHECK: %PIX_Constant_Color_Value0 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 0 +// CHECK: %PIX_Constant_Color_Value1 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 1 +// CHECK: %PIX_Constant_Color_Value2 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 2 +// CHECK: %PIX_Constant_Color_Value3 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 3 + +// Narrow to half: +// CHECK: %PIX_Constant_Color_ValueNarrowed0 = fptrunc float %PIX_Constant_Color_Value0 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed1 = fptrunc float %PIX_Constant_Color_Value1 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed2 = fptrunc float %PIX_Constant_Color_Value2 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed3 = fptrunc float %PIX_Constant_Color_Value3 to half + +// Store SV_Target0: +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half %PIX_Constant_Color_ValueNarrowed0) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half %PIX_Constant_Color_ValueNarrowed1) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half %PIX_Constant_Color_ValueNarrowed2) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half %PIX_Constant_Color_ValueNarrowed3) + +[RootSignature("")] +min16float4 main() : SV_Target { + return min16float4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl new file mode 100644 index 0000000000..1addc05a48 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl @@ -0,0 +1,29 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor | %FileCheck %s + +// MRT at ps_6_0 without -enable-16bit-types: RTV0 is min16float4, RTV1 is +// float4. The override applies to SV_Target0 only. + +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// RTV1 stays unchanged: +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 0, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 1, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 2, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 3, float 0.000000e+00) + +struct RTOut +{ + min16float4 h : SV_Target; + float4 c : SV_Target1; +}; + +[RootSignature("")] +RTOut main() { + RTOut rtOut; + rtOut.h = min16float4(0, 0, 0, 0); + rtOut.c = float4(0.f, 0.f, 0.f, 0.f); + return rtOut; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl new file mode 100644 index 0000000000..f18c542636 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor,constant-red=8,constant-green=7,constant-blue=6,constant-alpha=5 | %FileCheck %s + +// A min16int SV_Target lowers to dx.op.storeOutput.i16 at ps_6_0 without +// -enable-16bit-types. + +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 0, i16 8) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 1, i16 7) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 2, i16 6) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 3, i16 5) + +// CHECK-NOT: declare void @dx.op.storeOutput.f16 +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +min16int4 main() : SV_Target { + return min16int4(1, 2, 3, 4); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 0fa134c98f..f71742fe58 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -160,7 +160,10 @@ class PixTest : public ::testing::Test { TEST_METHOD(ToolsUav_RootSignatureSerializationFailurePreservesSignature) TEST_METHOD(ConstantColor_UnusedIntOverloadIsErased) TEST_METHOD(ConstantColor_NoTargetOverloadsAreErased) + TEST_METHOD(ConstantColor_FromConstantBufferIsWellFormed) TEST_METHOD(RemoveDiscards_UnusedDiscardOverloadIsErased) + TEST_METHOD(ReduceMSAAToSingleSample_SM66) + TEST_METHOD(ReduceMSAAToSingleSample_HalfLoad) TEST_METHOD(OperationCacheCleanup_RemovesErasedFunctions) TEST_METHOD(DynamicResourceCleanup_VisitorStopsEarly) @@ -3732,6 +3735,121 @@ float4 main() : SV_Target "discard removal with no discard"); } +TEST_F(PixTest, ConstantColor_FromConstantBufferIsWellFormed) { + const char *source = R"x( +float4 main(float4 position : SV_Position) : SV_Target +{ + return position; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-hlsl-dxil-constantColor,mod-mode=1"); + + // The CBuffer symbol must be a pointer to the struct so ValidateCBuffer + // can reach the annotation. + CComPtr pAssembler; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcAssembler, &pAssembler)); + CComPtr pAssembleResult; + VERIFY_SUCCEEDED( + pAssembler->AssembleToContainer(output.Module, &pAssembleResult)); + HRESULT assembleStatus; + VERIFY_SUCCEEDED(pAssembleResult->GetStatus(&assembleStatus)); + VERIFY_SUCCEEDED(assembleStatus); + + CComPtr pNewContainer; + VERIFY_SUCCEEDED(pAssembleResult->GetResult(&pNewContainer)); + + // The CBuffer resource record field 6 is size in bytes; a float4 row + // is 16 bytes. + auto lines = Tokenize(Disassemble(pNewContainer).c_str(), "\n"); + bool foundConstantColorCBuffer = false; + for (auto const &line : lines) { + if (line.find("!\"PIX_ConstantColorCBName\"") == std::string::npos) + continue; + auto fields = Tokenize(line.c_str(), ","); + VERIFY_IS_TRUE(fields.size() > 6); + // Field 1 is the global symbol; it must be a pointer to the CB struct. + VERIFY_ARE_NOT_EQUAL(std::string::npos, fields[1].find('*')); + VERIFY_ARE_EQUAL(16, atoi(fields[6].c_str() + fields[6].find("i32 ") + 4)); + foundConstantColorCBuffer = true; + } + VERIFY_IS_TRUE(foundConstantColorCBuffer); + + // The struct annotation names the float4 row in the reflection header. + bool foundStructAnnotation = false; + for (auto const &line : lines) { + if (line.find("struct PIX_ConstantColorCB_Type") != std::string::npos) + foundStructAnnotation = true; + } + VERIFY_IS_TRUE(foundStructAnnotation); + + VerifyInstrumentedModuleIsValid(pNewContainer, + "constant-colour from constant buffer"); +} + +static void +VerifyMSAALoadSampleWasReduced(std::vector const &lines, + const char *textureLoadOverload, + const char *originalSampleIndex) { + bool foundTextureLoad = false; + for (auto const &line : lines) { + if (line.find(" call ") == std::string::npos || + line.find(textureLoadOverload) == std::string::npos) { + continue; + } + + foundTextureLoad = true; + VERIFY_ARE_EQUAL(std::string::npos, line.find(originalSampleIndex)); + VERIFY_ARE_NOT_EQUAL(std::string::npos, line.find(", i32 0,")); + } + VERIFY_IS_TRUE(foundTextureLoad); +} + +TEST_F(PixTest, ReduceMSAAToSingleSample_SM66) { + if (m_ver.SkipDxilVersion(1, 6)) + return; + + // SM 6.6 lowers the resource handle through annotateHandle. + const char *source = R"x( +Texture2DMS tex : register(t0); +float4 main(float4 position : SV_Position) : SV_Target +{ + return tex.Load(int2(position.xy), 3); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); + auto output = RunSinglePass(compiled, L"-hlsl-dxil-reduce-msaa-to-single"); + auto lines = Tokenize(Disassemble(output.Module).c_str(), "\n"); + + VerifyMSAALoadSampleWasReduced(lines, "dx.op.textureLoad.f32", ", i32 3,"); + VerifyInstrumentedModuleIsValid(output.Module, + "MSAA reduction on SM 6.6 handle"); +} + +TEST_F(PixTest, ReduceMSAAToSingleSample_HalfLoad) { + if (m_ver.SkipDxilVersion(1, 2)) + return; + + // Texture2DMS.Load lowers to dx.op.textureLoad.f16. + const char *source = R"x( +Texture2DMS tex : register(t0); +float4 main(float4 position : SV_Position) : SV_Target +{ + half4 color = tex.Load(int2(position.xy), 2); + return float4(color); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_2", + {L"-Od", L"-enable-16bit-types"}); + auto output = RunSinglePass(compiled, L"-hlsl-dxil-reduce-msaa-to-single"); + auto lines = Tokenize(Disassemble(output.Module).c_str(), "\n"); + + VerifyMSAALoadSampleWasReduced(lines, "dx.op.textureLoad.f16", ", i32 2,"); + VerifyInstrumentedModuleIsValid(output.Module, + "MSAA reduction on 16-bit texture load"); +} + TEST_F(PixTest, OperationCacheCleanup_RemovesErasedFunctions) { const char *source = R"x( float4 main() : SV_Target From 8658126bcf86d0512bc975abe6de00011d117dbe Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 25 Aug 2026 00:13:20 -0700 Subject: [PATCH 10/39] [PIX] Fix debug variable storage layout The debug passes tell PIX where each variable lives in the shadow storage. Four defects make that information disagree with the shader. The offset calculation ignores the full extent of an array, the padding in an aggregate, the position of a bitfield, and the layout of a matrix. An offset can therefore point into the storage of another variable. A dynamically indexed write to an alloca takes its register span from the length of the array. DxilAnnotateWithVirtualRegister records the true span in !pix-alloca-reg-write. The two values disagree for an array of aggregates, so a dynamic index can be limited against the wrong bound. IsAllocaRegisterWrite examines only one level of the ancestor GEP chain. A value three or more aggregates deep receives no metadata, so the debugger shows a stale value or no value. The function also uses a member index without a bound check and without a safe cast. The pass finds the storage of an embedded array by the name of the debug variable. DXC can flatten a multi-dimensional array and rename the module global. The debug variable keeps its name, so the lookup misses the global and every shadow store for that array is dropped. Two decisions are worth attention. IsAllocaRegisterWrite returns false for a member index that is not constant or is out of range, because a guess would attach metadata the debug record cannot honour. A backward move of the layout writes a message and continues, and a debug build does not stop the process. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilAnnotateWithVirtualRegister.cpp | 97 +++-- .../DxilDbgValueToDbgDeclare.cpp | 222 +++++++--- .../DxilDebugInstrumentation.cpp | 29 +- .../DbgValueToDbgDeclare_16bit_bitfields.hlsl | 27 ++ ...gValueToDbgDeclare_16bit_tail_padding.hlsl | 37 ++ ...ueToDbgDeclare_aggregate_tail_padding.hlsl | 36 ++ ...eToDbgDeclare_array_of_padded_structs.hlsl | 44 ++ .../DbgValueToDbgDeclare_constant_local.hlsl | 18 + ...ueToDbgDeclare_front_and_tail_padding.hlsl | 50 +++ ...alueToDbgDeclare_matrix_after_padding.hlsl | 37 ++ ...lueToDbgDeclare_mixed_width_bitfields.hlsl | 31 ++ .../DbgValueToDbgDeclare_multidim_array.hlsl | 19 + ...ToDbgDeclare_nested_aggregate_padding.hlsl | 46 ++ ...bugInstrumentation_dynamic_index_span.hlsl | 24 ++ tools/clang/unittests/HLSL/PixTest.cpp | 396 +++++++++++++++++- 15 files changed, 992 insertions(+), 121 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl diff --git a/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp b/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp index 88f696b7fa..196da98c87 100644 --- a/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp +++ b/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp @@ -354,51 +354,70 @@ bool DxilAnnotateWithVirtualRegister::IsAllocaRegisterWrite( uint32_t precedingMemberCount = 0; auto *Alloca = llvm::dyn_cast(pGEP->getPointerOperand()); if (Alloca == nullptr) { - // In the case of vector types (floatN, matrixNxM), the pointer operand - // will actually point to another element pointer instruction. But this - // isn't a recursive thing- we only need to check these two levels. - if (auto *pPointerGEP = llvm::dyn_cast( - pGEP->getPointerOperand())) { - Alloca = - llvm::dyn_cast(pPointerGEP->getPointerOperand()); - if (Alloca == nullptr) { + // The pointer operand can itself be a GEP whenever the value being + // written is nested in more than one aggregate: a vector member of a + // struct (floatN, matrixNxM), or a struct member of a struct. Walk + // the whole chain of ancestor GEPs, outermost first, and require it + // to bottom out at an alloca. + llvm::SmallVector AncestorGEPs; + llvm::Value *PointerOperand = pGEP->getPointerOperand(); + while (auto *pAncestorGEP = + llvm::dyn_cast(PointerOperand)) { + AncestorGEPs.push_back(pAncestorGEP); + PointerOperand = pAncestorGEP->getPointerOperand(); + } + + Alloca = llvm::dyn_cast(PointerOperand); + if (Alloca == nullptr) { + return false; + } + + // Each level contributes the flattened count of whatever members precede + // the one it selects, so the offsets accumulate down the chain. + for (auto *pAncestorGEP : AncestorGEPs) { + auto *pStructType = llvm::dyn_cast( + pAncestorGEP->getPointerOperandType()->getPointerElementType()); + if (pStructType == nullptr) { + continue; + } + if (pAncestorGEP->getNumOperands() < 3) { + continue; + } + auto *pStructMember = + llvm::dyn_cast(pAncestorGEP->getOperand(2)); + if (pStructMember == nullptr) { + // A dynamically selected member has no constant offset; guessing + // zero would attribute the write to the wrong register. return false; } - // And of course the member we're after might not be at the beginning of - // any containing struct: - if (auto *pStructType = llvm::dyn_cast( - pPointerGEP->getPointerOperandType() - ->getPointerElementType())) { - auto *pStructMember = - llvm::dyn_cast(pPointerGEP->getOperand(2)); - uint64_t memberIndex = pStructMember->getLimitedValue(); - for (uint64_t i = 0; i < memberIndex; ++i) { - precedingMemberCount += - CountStructMembers(pStructType->getStructElementType(i)); - } + uint64_t memberIndex = pStructMember->getLimitedValue(); + if (memberIndex > pStructType->getStructNumElements()) { + return false; + } + for (uint64_t i = 0; i < memberIndex; ++i) { + precedingMemberCount += + CountStructMembers(pStructType->getStructElementType(i)); } + } - // And the source pointer may be a vector (floatn) type, - // and if so, that's another offset to consider. - llvm::Type *DestType = pGEP->getPointerOperand()->getType(); - // We expect this to be a pointer type (it's a GEP after all): - if (DestType->isPointerTy()) { - llvm::Type *PointedType = DestType->getPointerElementType(); - // Being careful to check num operands too in order to avoid false - // positives: - if (PointedType->isVectorTy() && pGEP->getNumOperands() == 3) { - // Fetch the second deref (in operand 2). - // (the first derefs the pointer to the "floatn", - // and the second denotes the index into the floatn.) - llvm::Value *vectorIndex = pGEP->getOperand(2); - if (auto *constIntIIndex = - llvm::cast(vectorIndex)) { - precedingMemberCount += constIntIIndex->getLimitedValue(); - } + // And the source pointer may be a vector (floatn) type, + // and if so, that's another offset to consider. + llvm::Type *DestType = pGEP->getPointerOperand()->getType(); + // We expect this to be a pointer type (it's a GEP after all): + if (DestType->isPointerTy()) { + llvm::Type *PointedType = DestType->getPointerElementType(); + // Being careful to check num operands too in order to avoid false + // positives: + if (PointedType->isVectorTy() && pGEP->getNumOperands() == 3) { + // Fetch the second deref (in operand 2). + // (the first derefs the pointer to the "floatn", + // and the second denotes the index into the floatn.) + llvm::Value *vectorIndex = pGEP->getOperand(2); + if (auto *constIntIIndex = + llvm::dyn_cast(vectorIndex)) { + precedingMemberCount += constIntIIndex->getLimitedValue(); } } - } else { - return false; } } diff --git a/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp b/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp index 9ddbe876b5..15a7e6c666 100644 --- a/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp +++ b/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp @@ -145,6 +145,22 @@ class OffsetManager { } } + // Move the aligned offset forward without adding padding to the packed + // offset. Overlapping debug information cannot move storage mappings + // backward. + void AdvanceAlignedOffsetTo(OffsetInBits AlignedOffset) { + if (AlignedOffset > m_CurrentAlignedOffset) { + VALUE_TO_DECLARE_LOG("Advancing aligned offset from %d to %d", + m_CurrentAlignedOffset, AlignedOffset); + m_CurrentAlignedOffset = AlignedOffset; + } else if (AlignedOffset < m_CurrentAlignedOffset) { + VALUE_TO_DECLARE_LOG("Refusing to move aligned offset back from %d to %d", + m_CurrentAlignedOffset, AlignedOffset); + // Keep existing mappings monotonic when debug information overlaps. + return; + } + } + // Add is used to "add" an aggregate element (struct field, array element) // at the current aligned/packed offsets, bumping them by Ty's size. Offsets Add(llvm::DIBasicType *Ty, unsigned sizeOverride) { @@ -443,63 +459,61 @@ DescendTypeAndFindEmbeddedArrayElements(llvm::StringRef VariableName, } else if (auto *CompositeTy = llvm::dyn_cast(Ty)) { switch (CompositeTy->getTag()) { case llvm::dwarf::DW_TAG_array_type: { + // DXC flattens a multi-dimensional array to a single one-dimensional + // array in the module. The true array extent is the product of the + // dimensions. + uint64_t TotalElementCount = 1; + bool FoundSubrange = false; for (auto Element : CompositeTy->getElements()) { - // First element for an array is DISubrange - if (auto Subrange = llvm::dyn_cast(Element)) { - auto ElementTy = CompositeTy->getBaseType().resolve(EmptyMap); - if (auto *BasicTy = llvm::dyn_cast(ElementTy)) { - bool CorrectLowerOffset = AccumulatedMemberOffset == OffsetToSeek; - bool CorrectUpperOffset = - AccumulatedMemberOffset + - Subrange->getCount() * BasicTy->getSizeInBits() == - OffsetToSeek + SizeToSeek; - if (BasicTy != nullptr && CorrectLowerOffset && - CorrectUpperOffset) { - std::vector storage; - for (int64_t i = 0; i < Subrange->getCount(); ++i) { - auto ElementOffset = - AccumulatedMemberOffset + i * BasicTy->getSizeInBits(); - GlobalEmbeddedArrayElementStorage element; - element.Name = VariableName.str() + "." + std::to_string(i); - element.Offset = static_cast(ElementOffset); - element.Size = - static_cast(BasicTy->getSizeInBits()); - storage.push_back(std::move(element)); - } - return storage; - } - } + if (auto *Subrange = llvm::dyn_cast(Element)) { + TotalElementCount *= Subrange->getCount(); + FoundSubrange = true; + } + } + + if (!FoundSubrange) { + break; + } - // If we didn't succeed and return above, then we need to process each - // element in the array + auto ElementTy = CompositeTy->getBaseType().resolve(EmptyMap); + if (ElementTy == nullptr) { + break; + } + + if (auto *BasicTy = llvm::dyn_cast(ElementTy)) { + const bool CorrectLowerOffset = AccumulatedMemberOffset == OffsetToSeek; + const bool CorrectUpperOffset = + AccumulatedMemberOffset + + TotalElementCount * BasicTy->getSizeInBits() == + OffsetToSeek + SizeToSeek; + if (CorrectLowerOffset && CorrectUpperOffset) { std::vector storage; - for (int64_t i = 0; i < Subrange->getCount(); ++i) { - auto elementStorage = DescendTypeAndFindEmbeddedArrayElements( - VariableName, - AccumulatedMemberOffset + ElementTy->getSizeInBits() * i, - ElementTy, OffsetToSeek, SizeToSeek); - std::move(elementStorage.begin(), elementStorage.end(), - std::back_inserter(storage)); - } - if (!storage.empty()) { - return storage; + for (uint64_t i = 0; i < TotalElementCount; ++i) { + auto ElementOffset = + AccumulatedMemberOffset + i * BasicTy->getSizeInBits(); + GlobalEmbeddedArrayElementStorage element; + element.Name = VariableName.str() + "." + std::to_string(i); + element.Offset = static_cast(ElementOffset); + element.Size = static_cast(BasicTy->getSizeInBits()); + storage.push_back(std::move(element)); } + return storage; } } - for (auto Element : CompositeTy->getElements()) { - // First element for an array is DISubrange - if (auto Subrange = llvm::dyn_cast(Element)) { - auto ElementType = CompositeTy->getBaseType().resolve(EmptyMap); - for (int64_t i = 0; i < Subrange->getCount(); ++i) { - auto storage = DescendTypeAndFindEmbeddedArrayElements( - VariableName, - AccumulatedMemberOffset + ElementType->getSizeInBits() * i, - ElementType, OffsetToSeek, SizeToSeek); - if (!storage.empty()) { - return storage; - } - } - } + + // The array's elements are themselves aggregates, so descend into each of + // them in turn looking for the sought offset. + std::vector storage; + for (uint64_t i = 0; i < TotalElementCount; ++i) { + auto elementStorage = DescendTypeAndFindEmbeddedArrayElements( + VariableName, + AccumulatedMemberOffset + ElementTy->getSizeInBits() * i, ElementTy, + OffsetToSeek, SizeToSeek); + std::move(elementStorage.begin(), elementStorage.end(), + std::back_inserter(storage)); + } + if (!storage.empty()) { + return storage; } } break; case llvm::dwarf::DW_TAG_structure_type: @@ -554,11 +568,18 @@ GlobalStorageMap GatherGlobalEmbeddedArrayStorage(llvm::Module &M) { if (auto *DIGVDerivedType = llvm::dyn_cast(DIGVType)) { if (DIGVDerivedType->getTag() == llvm::dwarf::DW_TAG_member) { - // This type is embedded within the containing DIGSV type + // This type is embedded within the containing DIGSV type. + // A flattened multi-dimensional array member renames the module + // global but not the debug variable, so only the linkage name + // still identifies it. + llvm::StringRef GlobalName = DIGV->getLinkageName(); + if (GlobalName.empty()) { + GlobalName = DIGV->getName(); + } const llvm::DITypeIdentifierMap EmptyMap; auto *Ty = HLSLStruct->getType().resolve(EmptyMap); auto Storage = DescendTypeAndFindEmbeddedArrayElements( - DIGV->getName(), 0, Ty, DIGVDerivedType->getOffsetInBits(), + GlobalName, 0, Ty, DIGVDerivedType->getOffsetInBits(), DIGVDerivedType->getSizeInBits()); auto &ArrayStorage = ret[HLSLStruct].ArrayElementStorage; std::move(Storage.begin(), Storage.end(), @@ -617,7 +638,8 @@ bool DxilDbgValueToDbgDeclare::runOnModule(llvm::Module &M) { // lists. for (auto &instruction : instructions) { if (auto *Store = llvm::dyn_cast(instruction)) { - Changed = + // Preserve changes reported by every processed store. + Changed |= handleStoreIfDestIsGlobal(M, GlobalEmbeddedArrayStorage, Store); } } @@ -847,6 +869,33 @@ static bool IsDITypePointer(DIType *DTy, return false; } +static bool HasPointerBackedCompositeCopy(llvm::DbgValueInst *DbgValue, + llvm::DIType *Ty) { + const llvm::DITypeIdentifierMap EmptyMap; + llvm::DIType *UnaliasedTy = DITypePeelTypeAlias(Ty); + if (!llvm::isa(DbgValue->getValue()) || + !llvm::isa(UnaliasedTy)) { + return false; + } + + for (llvm::BasicBlock &Block : *DbgValue->getParent()->getParent()) { + for (llvm::Instruction &Instruction : Block) { + auto *OtherDbgValue = llvm::dyn_cast(&Instruction); + if (OtherDbgValue == nullptr || OtherDbgValue == DbgValue || + !llvm::isa(OtherDbgValue->getValue())) { + continue; + } + + llvm::DIType *OtherTy = + OtherDbgValue->getVariable()->getType().resolve(EmptyMap); + if (OtherTy != nullptr && DITypePeelTypeAlias(OtherTy) == UnaliasedTy) { + return true; + } + } + } + return false; +} + void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, llvm::DbgValueInst *DbgValue) { VALUE_TO_DECLARE_LOG("DbgValue named %s", DbgValue->getName().str().c_str()); @@ -908,6 +957,11 @@ void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, } } + if (HasPointerBackedCompositeCopy(DbgValue, Ty)) { + VALUE_TO_DECLARE_LOG("Using pointer-backed composite storage"); + return; + } + auto &Register = m_Registers[Variable]; if (Register == nullptr) { Register.reset(new VariableRegisters( @@ -932,6 +986,10 @@ void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, const OffsetInBits InitialOffset = PackedOffsetFromVar; auto *insertPt = llvm::dyn_cast(ValueFromDbgInst); + if (insertPt == nullptr) { + // Constants and arguments are available at the dbg.value location. + insertPt = DbgValue; + } if (insertPt != nullptr && !llvm::isa(insertPt)) { insertPt = insertPt->getNextNode(); // Drivers may crash if phi nodes aren't always at the top of a block, @@ -965,11 +1023,28 @@ void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, continue; } - if (AllocaInst->getAllocatedType()->getArrayElementType() == - VO.m_V->getType()) { - auto *GEP = B.CreateGEP(AllocaInst, {Zero, Zero}); - B.CreateStore(VO.m_V, GEP); + llvm::Type *ShadowElementType = + AllocaInst->getAllocatedType()->getArrayElementType(); + llvm::Value *ValueToStore = VO.m_V; + if (ShadowElementType != ValueToStore->getType()) { + // Emit a bitcast to match the shadow alloca element type when the + // shader reinterprets the bits without converting them. + const llvm::DataLayout &DataLayout = M.getDataLayout(); + const bool SameWidth = + !ShadowElementType->isAggregateType() && + !ValueToStore->getType()->isAggregateType() && + !ShadowElementType->isPointerTy() && + !ValueToStore->getType()->isPointerTy() && + DataLayout.getTypeSizeInBits(ShadowElementType) == + DataLayout.getTypeSizeInBits(ValueToStore->getType()); + if (!SameWidth) { + continue; + } + ValueToStore = B.CreateBitCast(ValueToStore, ShadowElementType); } + + auto *GEP = B.CreateGEP(AllocaInst, {Zero, Zero}); + B.CreateStore(ValueToStore, GEP); } } } @@ -1158,7 +1233,13 @@ void VariableRegisters::PopulateAllocaMap(llvm::DIType *Ty) { case llvm::dwarf::DW_TAG_enumeration_type: { auto *baseType = CompositeTy->getBaseType().resolve(EmptyMap); if (baseType != nullptr) { + const OffsetInBits EnumerationStart = + m_Offsets.GetCurrentAlignedOffset(); PopulateAllocaMap(baseType); + // Advance to the enumeration's own declared size. + m_Offsets.AdvanceAlignedOffsetTo( + EnumerationStart + + static_cast(CompositeTy->getSizeInBits())); } else { m_Offsets.AlignToAndAddUnhandledType(CompositeTy); } @@ -1228,9 +1309,10 @@ void VariableRegisters::PopulateAllocaMap_BasicType(llvm::DIBasicType *Ty, auto *Storage = GetMetadataAsValue(llvm::ValueAsMetadata::get(Alloca)); auto *Variable = GetMetadataAsValue(m_Variable); - auto *Expression = GetMetadataAsValue( - GetDIExpression(Ty, sizeOverride == 0 ? offsets.Aligned : offsets.Packed, - GetVariableSizeInbits(m_Variable), sizeOverride)); + // Describe the aligned offset in the bit_piece so it agrees with the + // debug-info field's declared offset. + auto *Expression = GetMetadataAsValue(GetDIExpression( + Ty, offsets.Aligned, GetVariableSizeInbits(m_Variable), sizeOverride)); auto *DbgDeclare = m_B.CreateCall(m_DbgDeclareFn, {Storage, Variable, Expression}); DbgDeclare->setDebugLoc(m_dbgLoc); @@ -1261,7 +1343,6 @@ void VariableRegisters::PopulateAllocaMap_ArrayType(llvm::DICompositeType *Ty) { } const SizeInBits ArraySizeInBits = Ty->getSizeInBits(); - (void)ArraySizeInBits; const llvm::DITypeIdentifierMap EmptyMap; llvm::DIType *ElementTy = Ty->getBaseType().resolve(EmptyMap); @@ -1274,18 +1355,25 @@ void VariableRegisters::PopulateAllocaMap_ArrayType(llvm::DICompositeType *Ty) { // in bits. m_Offsets.AlignTo(ElementTy); + const OffsetInBits ArrayStart = m_Offsets.GetCurrentAlignedOffset(); + for (unsigned i = 0; i < NumElements; ++i) { // This is only needed if ElementTy's size is not a multiple of // its natural alignment. m_Offsets.AlignTo(ElementTy); PopulateAllocaMap(ElementTy); } + + // The elements only account for the bits they occupy, which stops short + // of the array's real end for a padded element type. Advance to the end. + m_Offsets.AdvanceAlignedOffsetTo(ArrayStart + ArraySizeInBits); } void VariableRegisters::PopulateAllocaMap_StructType( llvm::DICompositeType *Ty) { VALUE_TO_DECLARE_LOG("Struct type : %s, size %d", Ty->getName().str().c_str(), Ty->getSizeInBits()); + const SizeInBits StructSizeInBits = Ty->getSizeInBits(); std::map SortedMembers; if (!SortMembers(Ty, &SortedMembers)) { m_Offsets.AlignToAndAddUnhandledType(Ty); @@ -1294,7 +1382,6 @@ void VariableRegisters::PopulateAllocaMap_StructType( m_Offsets.AlignTo(Ty); const OffsetInBits StructStart = m_Offsets.GetCurrentAlignedOffset(); - (void)StructStart; const llvm::DITypeIdentifierMap EmptyMap; for (auto OffsetAndMember : SortedMembers) { @@ -1310,6 +1397,10 @@ void VariableRegisters::PopulateAllocaMap_StructType( // than the type in which it resides). If we were to take // the base type, then the information about the member's // size would be lost + // + // The AlignTo above is a no-op for a bitfield, so snap to the declared + // offset here to ensure it aligns with the debug info. + m_Offsets.AdvanceAlignedOffsetTo(StructStart + OffsetAndMember.first); PopulateAllocaMap(OffsetAndMember.second); } else { if (OffsetAndMember.second->getAlignInBits() == @@ -1326,6 +1417,11 @@ void VariableRegisters::PopulateAllocaMap_StructType( } } } + + // The members between them only account for the bits they occupy, which stops + // short of the struct's real end whenever the struct's alignment requires + // tail padding. Advance to the struct's full size. + m_Offsets.AdvanceAlignedOffsetTo(StructStart + StructSizeInBits); } // HLSL Change: remove unused function diff --git a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp index a40acfe860..d533e0c95b 100644 --- a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp @@ -235,6 +235,7 @@ struct InstructionAndType { DebugShaderModifierRecordType Type; std::uint32_t RegisterNumber; std::uint32_t AllocaBase; + std::uint32_t AllocaRegisterSize = 0; Value *AllocaWriteIndex = nullptr; std::optional ConstantAllocaStoreValue; }; @@ -993,11 +994,10 @@ std::optional DxilDebugInstrumentation::addStoreStepDebugEntry(BuilderContext *BC, StoreInst *Inst) { std::uint32_t ValueOrdinalBase; - std::uint32_t UnusedValueOrdinalSize; + std::uint32_t ValueOrdinalSize; llvm::Value *ValueOrdinalIndex; - if (!pix_dxil::PixAllocaRegWrite::FromInst(Inst, &ValueOrdinalBase, - &UnusedValueOrdinalSize, - &ValueOrdinalIndex)) { + if (!pix_dxil::PixAllocaRegWrite::FromInst( + Inst, &ValueOrdinalBase, &ValueOrdinalSize, &ValueOrdinalIndex)) { return std::nullopt; } @@ -1019,6 +1019,7 @@ DxilDebugInstrumentation::addStoreStepDebugEntry(BuilderContext *BC, ret.Type = *Type; ret.RegisterNumber = RegNum; ret.AllocaBase = ValueOrdinalBase; + ret.AllocaRegisterSize = ValueOrdinalSize; ret.AllocaWriteIndex = ValueOrdinalIndex; return ret; } @@ -1029,6 +1030,7 @@ DxilDebugInstrumentation::addStoreStepDebugEntry(BuilderContext *BC, ret.InstructionOrdinal = InstNum; ret.Type = *Type; ret.AllocaBase = ValueOrdinalBase; + ret.AllocaRegisterSize = ValueOrdinalSize; ret.AllocaWriteIndex = ValueOrdinalIndex; switch (ValueAsConst->getType()->getTypeID()) { @@ -1356,21 +1358,18 @@ DxilDebugInstrumentation::FindInstrumentableInstructionsInBlock( IndexingToken = "s"; // static indexing, no debug output required } else { IndexingToken = "d"; // dynamic indexing - int MaxArraySize = 1; - if (auto *Store = dyn_cast(&Inst)) { - if (auto *GEP = - dyn_cast(Store->getPointerOperand())) { - if (auto *Alloca = - dyn_cast(GEP->getPointerOperand())) { - MaxArraySize = - Alloca->getAllocatedType()->getArrayNumElements(); - } - } - } + // The register span for a dynamic write comes from the + // !pix-alloca-reg-write metadata the annotation pass attached to + // this instruction, so it always matches the virtual-register + // numbering the annotation pass assigned. + uint32_t MaxArraySize = std::max(1u, IandT->AllocaRegisterSize); RegisterOrStaticIndex = std::to_string(IandT->AllocaBase) + "-" + std::to_string(MaxArraySize); DebugOutputForThisInstruction.ValueToWriteToDebugMemory = IandT->AllocaWriteIndex; + // Dynamic alloca records store the i32 index as a four-byte payload. + DebugOutputForThisInstruction.ValueType = + DebugShaderModifierRecordTypeDXILStepUint32; } } else { IndexingToken = "a"; // meaning an SSA assignment diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl new file mode 100644 index 0000000000..f60df66212 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl @@ -0,0 +1,27 @@ +// RUN: %dxc -Tcs_6_6 -enable-16bit-types -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify that 16-bit and 32-bit bitfields use their declared storage units. + +RWByteAddressBuffer RawUAV : register(u0); + +struct HalfBitfield +{ + uint16_t Small : 5; + uint32_t Wide : 20; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 5) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 32, 20) + +// CHECK: store i16 %{{[^,]+}}, i16* +// CHECK: store i32 %{{[^,]+}}, i32* + +[numthreads(1, 1, 1)] +void main() +{ + HalfBitfield bitfield; + bitfield.Small = (uint16_t)RawUAV.Load(2 * 4); + bitfield.Wide = RawUAV.Load(3 * 4); + + RawUAV.Store(0, bitfield.Small + bitfield.Wide); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl new file mode 100644 index 0000000000..f0a5f5291d --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Tcs_6_6 -enable-16bit-types -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify a 16-bit member after a tail-padded 64-bit struct. + +RWByteAddressBuffer RawUAV : register(u0); + +struct HalfTail +{ + float Wide; + float16_t Small; +}; + +struct Holder +{ + HalfTail Padded; + float16_t Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 32, 16) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 16) + +// CHECK: store float +// CHECK: store half +// CHECK: store half + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Padded.Wide = (float)RawUAV.Load(2 * 4); + holder.Padded.Small = (float16_t)RawUAV.Load(3 * 4); + holder.Trailing = (float16_t)RawUAV.Load(4 * 4); + + RawUAV.Store(0, holder.Padded.Wide + (float)holder.Padded.Small + + (float)holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl new file mode 100644 index 0000000000..4180e594f9 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl @@ -0,0 +1,36 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify a member after a 128-bit struct with 32 bits of tail padding. + +RWByteAddressBuffer RawUAV : register(u0); + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + TailPadded Padded; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 32) + +// CHECK: store double +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Padded.Wide = (double)RawUAV.Load(2 * 4); + holder.Padded.Narrow = (float)RawUAV.Load(3 * 4); + holder.Trailing = (float)RawUAV.Load(7 * 4); + + RawUAV.Store(0, (float)holder.Padded.Wide + holder.Padded.Narrow + holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl new file mode 100644 index 0000000000..001a1353cc --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl @@ -0,0 +1,44 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify an array of 128-bit padded structs followed by another member. + +RWByteAddressBuffer RawUAV : register(u0); + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + TailPadded Elements[2]; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 256, 32) + +// CHECK: store double +// CHECK: store float +// CHECK: store double +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Elements[0].Wide = (double)RawUAV.Load(2 * 4); + holder.Elements[0].Narrow = (float)RawUAV.Load(3 * 4); + holder.Elements[1].Wide = (double)RawUAV.Load(4 * 4); + holder.Elements[1].Narrow = (float)RawUAV.Load(5 * 4); + holder.Trailing = (float)RawUAV.Load(7 * 4); + + RawUAV.Store(0, (float)holder.Elements[0].Wide + holder.Elements[0].Narrow + + (float)holder.Elements[1].Wide + holder.Elements[1].Narrow + + holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl new file mode 100644 index 0000000000..38d82b5b92 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +RWByteAddressBuffer RawUAV : register(u0); + +[numthreads(1, 1, 1)] +void main() +{ + float bias = 0.5; + bool hitFlag = true; + RawUAV.Store(0, asuint(bias + (hitFlag ? 2.0 : 3.0))); +} + +// CHECK: %[[hitFlag:.*]] = alloca [1 x i32] +// CHECK: %[[bias:.*]] = alloca [1 x float] +// CHECK: %[[bias_gep:.*]] = getelementptr [1 x float], [1 x float]* %[[bias]], i32 0, i32 0 +// CHECK: store float 5.000000e-01, float* %[[bias_gep]] +// CHECK: %[[hitFlag_gep:.*]] = getelementptr [1 x i32], [1 x i32]* %[[hitFlag]], i32 0, i32 0 +// CHECK: store i32 1, i32* %[[hitFlag_gep]] diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl new file mode 100644 index 0000000000..fc5a6e216b --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl @@ -0,0 +1,50 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify declared offsets for front-padded and tail-padded structs. + +RWByteAddressBuffer RawUAV : register(u0); + +struct FrontPadded +{ + float Narrow; + double Wide; +}; + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + FrontPadded Front; + TailPadded Tail; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 256, 32) + +// CHECK: store float +// CHECK: store double +// CHECK: store double +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Front.Narrow = (float)RawUAV.Load(2 * 4); + holder.Front.Wide = (double)RawUAV.Load(3 * 4); + holder.Tail.Wide = (double)RawUAV.Load(4 * 4); + holder.Tail.Narrow = (float)RawUAV.Load(5 * 4); + holder.Trailing = (float)RawUAV.Load(6 * 4); + + RawUAV.Store(0, holder.Front.Narrow + (float)holder.Front.Wide + + (float)holder.Tail.Wide + holder.Tail.Narrow + holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl new file mode 100644 index 0000000000..d678350e3d --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify matrix offsets after a 128-bit tail-padded struct. + +RWByteAddressBuffer RawUAV : register(u0); + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + TailPadded Padded; + float2x2 Mat; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 160, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 224, 32) + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Padded.Wide = (double)RawUAV.Load(2 * 4); + holder.Padded.Narrow = (float)RawUAV.Load(3 * 4); + holder.Mat = float2x2(RawUAV.Load(4 * 4), RawUAV.Load(5 * 4), + RawUAV.Load(6 * 4), RawUAV.Load(7 * 4)); + + RawUAV.Store(0, (float)holder.Padded.Wide + holder.Padded.Narrow + + holder.Mat._11 + holder.Mat._22); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl new file mode 100644 index 0000000000..1a72d36ef4 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl @@ -0,0 +1,31 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify that mixed-width bitfields use their declared storage units. + +RWByteAddressBuffer RawUAV : register(u0); + +struct MixedWidthBitfield +{ + uint32_t Leading : 5; + uint64_t Middle : 59; + uint32_t Trailing : 5; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 5) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 59) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 5) + +// CHECK: store i32 %{{[^,]+}}, i32* +// CHECK: store i64 %{{[^,]+}}, i64* +// CHECK: store i32 %{{[^,]+}}, i32* + +[numthreads(1, 1, 1)] +void main() +{ + MixedWidthBitfield bitfield; + bitfield.Leading = RawUAV.Load(9 * 4); + bitfield.Middle = RawUAV.Load(21 * 4); + bitfield.Trailing = RawUAV.Load(13 * 4); + + RawUAV.Store(0, (uint)(bitfield.Leading + bitfield.Middle + bitfield.Trailing)); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl new file mode 100644 index 0000000000..e4eb97405a --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl @@ -0,0 +1,19 @@ +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +static float MyArray[2][2] = { + { 1.0, 2.0 }, + { 3.0, 4.0 } +}; + +RWByteAddressBuffer RawUAV : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + RawUAV.Store(0, asuint(MyArray[tid.x][tid.y])); +} + +// Verify stores for every flattened element of the multidimensional array. +// CHECK: store float 2.000000e+00, float* +// CHECK: store float 3.000000e+00, float* +// CHECK: store float 4.000000e+00, float* diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl new file mode 100644 index 0000000000..fa9ed43a92 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl @@ -0,0 +1,46 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify declared offsets across nested tail-padded aggregates. + +RWByteAddressBuffer RawUAV : register(u0); + +struct Leaf +{ + double Wide; + float Narrow; +}; + +struct Middle +{ + Leaf Nested; + float AfterNested; +}; + +struct Root +{ + Middle Inner; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) + +// CHECK: store double +// CHECK: store float +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Root root; + root.Inner.Nested.Wide = (double)RawUAV.Load(2 * 4); + root.Inner.Nested.Narrow = (float)RawUAV.Load(3 * 4); + root.Inner.AfterNested = (float)RawUAV.Load(4 * 4); + root.Trailing = (float)RawUAV.Load(5 * 4); + + RawUAV.Store(0, (float)root.Inner.Nested.Wide + root.Inner.Nested.Narrow + + root.Inner.AfterNested + root.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl new file mode 100644 index 0000000000..65c20c4f7e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl @@ -0,0 +1,24 @@ +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-annotate-with-virtual-regs -hlsl-dxil-debug-instrumentation,UAVSize=1048576 | %FileCheck %s -check-prefix=SIZE +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-annotate-with-virtual-regs -hlsl-dxil-debug-instrumentation,UAVSize=1048576 | %FileCheck %s -check-prefix=NO-OLD + +RWByteAddressBuffer RawUAV : register(u0); + +[numthreads(1, 1, 1)] void main() { + double local_array[4]; + uint index = RawUAV.Load(0); + double value = asdouble(RawUAV.Load(4), RawUAV.Load(8)); + if (index < 4) { + local_array[index] = value; + } + RawUAV.Store(12, asuint((float)local_array[0])); +} + +// The store block reserves a 12-byte header and a 4-byte index payload. +// SIZE-LABEL: define void @main() +// SIZE: getelementptr inbounds [4 x double], [4 x double]* %{{[a-zA-Z0-9._]+}}, i32 0, i32 %{{[a-zA-Z0-9._]+}} +// SIZE: call i32 @dx.op.atomicBinOp.i32(i32 78, %dx.types.Handle {{.*}}, i32 0, i32 {{.*}}, i32 undef, i32 undef, i32 16) +// SIZE-LABEL: declare double @dx.op.makeDouble.f64 + +// NO-OLD-LABEL: define void @main() +// NO-OLD-NOT: i32 undef, i32 undef, i32 20) +// NO-OLD-LABEL: declare double @dx.op.makeDouble.f64 diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index f71742fe58..faf04b4aaf 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -37,6 +37,7 @@ #include "dxc/DXIL/DxilModule.h" #include "dxc/DXIL/DxilOperations.h" #include "dxc/DXIL/DxilSubobject.h" +#include "dxc/DxilPIXPasses/DxilPIXPasses.h" #include "dxc/Test/DxcTestUtils.h" #include "dxc/Test/HLSLTestData.h" @@ -52,6 +53,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringSwitch.h" +#include "llvm/AsmParser/Parser.h" #include "llvm/Bitcode/ReaderWriter.h" #include "llvm/IR/DebugInfo.h" #include "llvm/IR/Instructions.h" @@ -61,10 +63,12 @@ #include "llvm/IR/Module.h" #include "llvm/IR/ModuleSlotTracker.h" #include "llvm/IR/Operator.h" +#include "llvm/Pass.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MSFileSystem.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" +#include "llvm/Support/SourceMgr.h" #include #include <../lib/DxilDia/DxcPixLiveVariables_FragmentIterator.h> @@ -147,6 +151,11 @@ class PixTest : public ::testing::Test { TEST_METHOD(PixStructAnnotation_Inheritance) TEST_METHOD(PixStructAnnotation_ResourceAsMember) TEST_METHOD(PixStructAnnotation_WheresMyDbgValue) + TEST_METHOD(DbgValueToDbgDeclare_BackwardLayout) + TEST_METHOD(DebugInstrumentation_DynamicIndexSpanMatchesAllocaRegisterCount) + TEST_METHOD(PixDbgValueToDbgDeclare_MultiDimensionalStaticGlobalArray) + TEST_METHOD(AllocaRegisterWrite_DeepAggregateChainIsAnnotated) + TEST_METHOD(EntryBlockInjection_HandlesLabelledAndUnlabelledFirstBlock) TEST_METHOD(VirtualRegisters_InstructionCounts) TEST_METHOD(VirtualRegisters_AlignedOffsets) @@ -306,6 +315,42 @@ class PixTest : public ::testing::Test { std::vector Lines; }; + // Runs the virtual-register annotation pass over textual IR and returns the + // pass report. Textual IR builds a module shape that HLSL does not express. + std::vector RunAnnotationPassOnText(const std::string &irText) { + CComPtr pSource; + CreateBlobFromText(m_dllSupport, irText.c_str(), &pSource); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-S"); + Options.push_back(L"-opt-mod-passes"); + Options.push_back(L"-dxil-annotate-with-virtual-regs"); + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + pSource, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + return Tokenize(BlobToUtf8(pText).c_str(), "\n"); + } + + // Replaces the one occurrence of needle, and fails the test when the text + // does not hold exactly one. + static std::string ReplaceOnlyOccurrence(const std::string &text, + const std::string &needle, + const std::string &replacement) { + auto position = text.find(needle); + VERIFY_IS_TRUE(position != std::string::npos); + VERIFY_IS_TRUE(text.find(needle, position + needle.size()) == + std::string::npos); + std::string result = text; + result.replace(position, needle.size(), replacement); + return result; + } + SinglePassOutput RunSinglePass(IDxcBlob *dxil, LPCWSTR passOption) { CComPtr pOptimizer; VERIFY_SUCCEEDED( @@ -2095,9 +2140,9 @@ void main() auto Testables = TestStructAnnotationCase(hlsl, optimization); - // 2 in unoptimized case (one for each instance of smallPayload) - // 1 in optimized case (cuz p2 aliases over p) - VERIFY_IS_TRUE(Testables.OffsetAndSizes.size() >= 1); + // Each unoptimized source variable has storage. Optimized copies alias. + const size_t ExpectedCount = choice.IsOptimized ? 1u : 2u; + VERIFY_ARE_EQUAL(ExpectedCount, Testables.OffsetAndSizes.size()); for (const auto &os : Testables.OffsetAndSizes) { VERIFY_ARE_EQUAL(1u, os.countOfMembers); @@ -2105,10 +2150,78 @@ void main() VERIFY_ARE_EQUAL(32u, os.size); } - VERIFY_ARE_EQUAL(1u, Testables.AllocaWrites.size()); + VERIFY_ARE_EQUAL(ExpectedCount, Testables.AllocaWrites.size()); + for (size_t i = 0; i < ExpectedCount; ++i) { + ValidateAllocaWrite(Testables.AllocaWrites, i, "dummy"); + } } } +TEST_F(PixTest, DbgValueToDbgDeclare_BackwardLayout) { + const char *IR = R"( + %BadStruct = type { i64, i32 } + + define void @main() !dbg !5 { + entry: + %var = alloca %BadStruct, align 4 + call void @llvm.dbg.value(metadata %BadStruct* %var, i64 0, metadata !10, metadata !15), !dbg !16 + ret void + } + + declare void @llvm.dbg.value(metadata, i64, metadata, metadata) + + !llvm.dbg.cu = !{!0} + !llvm.module.flags = !{!3, !4} + + !0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: 1, subprograms: !2) + !1 = !DIFile(filename: "test.hlsl", directory: "/") + !2 = !{!5} + !3 = !{i32 2, !"Dwarf Version", i32 4} + !4 = !{i32 2, !"Debug Info Version", i32 3} + !5 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 1, type: !6, isLocal: false, isDefinition: true, scopeLine: 1, flags: DIFlagPrototyped, isOptimized: false, function: void ()* @main) + !6 = !DISubroutineType(types: !7) + !7 = !{null} + !8 = !DIBasicType(name: "int64", size: 64, align: 32, encoding: DW_ATE_signed) + !9 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed) + !10 = !DILocalVariable(tag: DW_TAG_auto_variable, name: "var", scope: !5, file: !1, line: 2, type: !11) + !11 = !DICompositeType(tag: DW_TAG_structure_type, name: "BadStruct", file: !1, line: 1, size: 96, align: 32, elements: !12) + !12 = !{!13, !14} + !13 = !DIDerivedType(tag: DW_TAG_member, name: "First", scope: !11, file: !1, line: 2, baseType: !8, size: 64, align: 32, offset: 0) + !14 = !DIDerivedType(tag: DW_TAG_member, name: "Second", scope: !11, file: !1, line: 3, baseType: !9, size: 16, align: 32, offset: 32) + !15 = !DIExpression() + !16 = !DILocation(line: 2, column: 1, scope: !5) + )"; + + llvm::LLVMContext Context; + llvm::SMDiagnostic Error; + std::unique_ptr Module = + llvm::parseAssemblyString(IR, Error, Context); + VERIFY_IS_NOT_NULL(Module.get()); + + std::unique_ptr Pass( + llvm::createDxilDbgValueToDbgDeclarePass()); + VERIFY_IS_TRUE(Pass->runOnModule(*Module)); + + std::vector> Pieces; + for (llvm::BasicBlock &Block : *Module->getFunction("main")) { + for (llvm::Instruction &Instruction : Block) { + if (auto *Declare = llvm::dyn_cast(&Instruction)) { + llvm::DIExpression *Expression = Declare->getExpression(); + VERIFY_IS_TRUE(Expression->isBitPiece()); + Pieces.emplace_back(Expression->getBitPieceOffset(), + Expression->getBitPieceSize()); + } + } + } + + std::sort(Pieces.begin(), Pieces.end()); + VERIFY_ARE_EQUAL(size_t(2), Pieces.size()); + VERIFY_ARE_EQUAL(uint64_t(0), Pieces[0].first); + VERIFY_ARE_EQUAL(uint64_t(64), Pieces[0].second); + VERIFY_ARE_EQUAL(uint64_t(64), Pieces[1].first); + VERIFY_ARE_EQUAL(uint64_t(16), Pieces[1].second); +} + TEST_F(PixTest, PixStructAnnotation_MixedSizes) { if (m_ver.SkipDxilVersion(1, 5)) return; @@ -4745,3 +4858,278 @@ float4 main(float4 pos : SV_Position) : SV_Target VerifyInstrumentedModuleIsValid( output.blob, "shader access tracking of a dynamically indexed resource"); } + +// Pulls the register span out of every dynamically-indexed alloca write the +// debug instrumentation pass reported. The per-block records it emits are +// semicolon-separated and a dynamic alloca write looks like +// +// ,,,d,- +// +// where the span is how many virtual registers the write could land in. +static std::vector +FindDynamicAllocaWriteSpans(std::vector const &passOutputLines) { + std::vector spans; + for (auto const &line : passOutputLines) { + for (auto const &record : Split(line, ';')) { + auto tokens = Split(record, ','); + if (tokens.size() < 5 || tokens[3] != "d") { + continue; + } + auto const dash = tokens[4].find('-'); + if (dash == std::string::npos) { + continue; + } + spans.push_back(atoi(tokens[4].substr(dash + 1).c_str())); + } + } + return spans; +} + +// PIX clamps a dynamic index to the span this record reports, so a span that +// undercounts the alloca hides every element past it. The span comes from the +// !pix-alloca-reg-write metadata the annotation pass attaches to the +// instruction, not from the alloca's LLVM array length, so it always matches +// the virtual-register numbering. +// +// DXC's SROA flattens every aggregate the front end emits, so today's shapes +// keep both derivations in agreement. This test guards against that ceasing +// to be true. +TEST_F(PixTest, + DebugInstrumentation_DynamicIndexSpanMatchesAllocaRegisterCount) { + struct Case { + char const *description; + char const *source; + int expectedSpan; + }; + + const Case cases[] = { + {"one-dimensional float array", R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + float values[8]; + for (uint i = 0; i < 8; ++i) values[i] = 0; + values[RawUAV.Load(0)] = 7; + RawUAV.Store(4, asuint(values[RawUAV.Load(8)])); +})x", + 8}, + {"two-dimensional array is flattened to one register run", R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + float m[4][4]; + for (uint i = 0; i < 4; ++i) for (uint j = 0; j < 4; ++j) m[i][j] = 0; + m[RawUAV.Load(0)][RawUAV.Load(4)] = 7; + RawUAV.Store(8, asuint(m[RawUAV.Load(12)][RawUAV.Load(16)])); +})x", + 16}, + {"array member of a struct", R"x( +RWByteAddressBuffer RawUAV : register(u0); +struct Container { float before; float values[8]; float after; }; +[numthreads(1, 1, 1)] +void main() +{ + Container c; + c.before = 1; + c.after = 2; + for (uint i = 0; i < 8; ++i) c.values[i] = 0; + c.values[RawUAV.Load(0)] = 7; + RawUAV.Store(4, asuint(c.values[RawUAV.Load(8)] + c.before + c.after)); +})x", + 8}, + {"dynamically indexed vector", R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + float3 v = float3(1, 2, 3); + v[RawUAV.Load(0)] = 7; + RawUAV.Store(4, asuint(v.x + v.y + v.z)); +})x", + 3}, + }; + + for (auto const &testCase : cases) { + WEX::Logging::Log::Comment( + WEX::Common::String().Format(L"%S", testCase.description)); + + auto compiled = Compile(m_dllSupport, testCase.source, L"cs_6_0", {L"-Od"}); + auto output = RunDebugPass(compiled); + auto spans = FindDynamicAllocaWriteSpans(output.lines); + + // If DXC ever stops emitting a dynamically-indexed alloca store for these + // shaders the test would otherwise quietly become a test of nothing. + VERIFY_IS_TRUE(spans.size() > 0); + for (int span : spans) { + VERIFY_ARE_EQUAL(testCase.expectedSpan, span); + } + } +} + +// Counts stores of the given value into a shadow alloca, i.e. stores whose +// destination is a local pointer rather than a module-scope global. +static uint32_t +CountStoresToAllocaOfValue(std::vector const &disassemblyLines, + const char *value) { + uint32_t count = 0; + for (auto const &line : disassemblyLines) { + if (line.find("store ") == std::string::npos) { + continue; + } + if (line.find(value) == std::string::npos) { + continue; + } + // A store into the original global names the global; the shadow stores this + // pass emits target an alloca reached through a local GEP. + if (line.find('@') != std::string::npos) { + continue; + } + count++; + } + return count; +} + +// A flattened multi-dimensional array member renames the module global but +// not the debug variable, so the pass gathers shadow storage by linkage +// name. Keying it on the debug name instead loses the shadow store for every +// write into the flattened array. +TEST_F(PixTest, PixDbgValueToDbgDeclare_MultiDimensionalStaticGlobalArray) { + const char *source = R"x( +RWByteAddressBuffer RawUAV : register(u0); +struct StaticGlobalHolder +{ + float twoD[2][3]; + float oneD[3]; + float count; +}; +static StaticGlobalHolder g_staticGlobalHolder; +[numthreads(1, 1, 1)] +void main() +{ + g_staticGlobalHolder.oneD[0] = 4.0; + g_staticGlobalHolder.oneD[1] = 5.0; + g_staticGlobalHolder.oneD[2] = 6.0; + g_staticGlobalHolder.twoD[1][0] = 40.0; + g_staticGlobalHolder.twoD[1][2] = 42.0; + g_staticGlobalHolder.count = 1; + + float accumulator = 0; + uint index = 0; + [loop] + while (true) + { + accumulator += g_staticGlobalHolder.twoD[index % 2][index % 3]; + accumulator += g_staticGlobalHolder.oneD[index % 3]; + if (index++ == 4) + { + break; + } + } + RawUAV.Store(64, asuint(accumulator + g_staticGlobalHolder.count)); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {L"-Od"}); + CComPtr dxilPart = FindModule(DFCC_ShaderDebugInfoDXIL, compiled); + auto output = RunValueToDeclarePass(dxilPart); + auto lines = Split(Disassemble(output.blob), '\n'); + + // The one-dimensional array in the same struct is the control: it is handled + // correctly whether or not the multi-dimensional case is. + VERIFY_ARE_EQUAL(1u, CountStoresToAllocaOfValue(lines, "4.000000e+00")); + // The two writes into the two-dimensional array are the point of the test. + VERIFY_ARE_EQUAL(1u, CountStoresToAllocaOfValue(lines, "4.000000e+01")); + VERIFY_ARE_EQUAL(1u, CountStoresToAllocaOfValue(lines, "4.200000e+01")); +} + +// Returns the module with the given instructions at the start of the entry +// point's first block. The disassembler prints a label for that block only +// when the block is named, and instructions placed ahead of a label would form +// a block with no terminator, so the injection follows the label when there is +// one and the definition's brace when there is not. +static std::string InjectIntoEntryBlock(const std::string &disassembly, + const std::string &instructions) { + const std::string definition = "define void @main() {"; + const std::string labelledDefinition = definition + "\nentry:"; + const std::string &anchor = + disassembly.find(labelledDefinition) != std::string::npos + ? labelledDefinition + : definition; + return PixTest::ReplaceOnlyOccurrence(disassembly, anchor, + anchor + "\n" + instructions); +} + +// Whether the disassembler labels an entry point's first block depends on +// whether the module kept the block's name, so the injection below pins its +// point against both forms rather than against the one this build produces. +TEST_F(PixTest, EntryBlockInjection_HandlesLabelledAndUnlabelledFirstBlock) { + const std::string instruction = " %injected = alloca float"; + + const std::string labelled = "define void @main() {\nentry:\n ret void\n}\n"; + VERIFY_ARE_EQUAL("define void @main() {\nentry:\n" + instruction + + "\n ret void\n}\n", + InjectIntoEntryBlock(labelled, instruction)); + + const std::string unlabelled = "define void @main() {\n ret void\n}\n"; + VERIFY_ARE_EQUAL("define void @main() {\n" + instruction + + "\n ret void\n}\n", + InjectIntoEntryBlock(unlabelled, instruction)); +} + +// A value nested three GEPs below the alloca needs the annotator to walk the +// whole ancestor chain, not just one level, before it can record the store's +// !pix-alloca-reg-write. DXC's SROA flattens aggregates before this pass +// runs, so this shape does not arise from HLSL; the module is constructed +// directly here. +TEST_F(PixTest, AllocaRegisterWrite_DeepAggregateChainIsAnnotated) { + auto compiled = Compile(m_dllSupport, R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + RawUAV.Store(0, 0); +})x", + L"cs_6_0", {L"-Od"}); + std::string disassembly = Disassemble(compiled); + + // An alloca of a struct nested three levels deep, then a GEP chain that + // descends every level to select a scalar, then a store into it. The store's + // pointer is three GEPs removed from the alloca. + std::string withDeepStore = InjectIntoEntryBlock( + disassembly, + " %deep = alloca { { { float, float } } }\n" + " %deep.l0 = getelementptr { { { float, float } } }, { { { " + "float, float } } }* %deep, i32 0, i32 0\n" + " %deep.l1 = getelementptr { { float, float } }, { { float, " + "float } }* %deep.l0, i32 0, i32 0\n" + " %deep.l2 = getelementptr { float, float }, { float, float " + "}* %deep.l1, i32 0, i32 1\n" + " store float 1.000000e+00, float* %deep.l2\n"); + + std::vector lines = RunAnnotationPassOnText(withDeepStore); + + bool allocaRegistered = false; + bool storeFound = false; + bool storeAnnotated = false; + for (const std::string &line : lines) { + if (line.find("%deep = alloca") != std::string::npos && + line.find("pix-alloca-reg") != std::string::npos) { + allocaRegistered = true; + } + if (line.find("store float 1.000000e+00, float* %deep.l2") != + std::string::npos) { + storeFound = true; + if (line.find("pix-alloca-reg-write") != std::string::npos) { + storeAnnotated = true; + } + } + } + + // The alloca is registered, so the shape reached the pass and the store below + // is the thing under test rather than an artifact of it being skipped. + VERIFY_IS_TRUE(allocaRegistered); + VERIFY_IS_TRUE(storeFound); + // The store three GEPs deep still carries its alloca-register-write. + VERIFY_IS_TRUE(storeAnnotated); +} From cf36a3e90ea3c55f7ecd9f06fd04553227e61ba1 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 25 Aug 2026 07:41:10 -0700 Subject: [PATCH 11/39] [PIX] Fix pixel-hit signature instrumentation PIX must know which signature row carries SV_Position, so that it can match a pixel with its instrumentation record. The pass has one option for this row and no way to state whether the row is a requirement or a guess. An unverified row can evict a real interpolant. Moving SV_Position makes the input signature larger, so the pass must repack the displaced elements. A repack that cannot complete leaves the signature partly rewritten. Signature growth also invalidates the ViewID state, which the pass keeps. The pass scales the pixel-counter element offset to a byte offset and then limits it. An element index large enough to overflow the multiplication wraps before the limit applies, so an out-of-range pixel is attributed to byte 0, which is pixel 0. A render-target width or pixel count that is zero, negative, or too large for 32-bit offset arithmetic produces a shader that wraps silently. Two options carry the caller's intent. preferred-sv-position-row is a hint, and the pass uses the row only when the row is free. required-sv-position-row is a requirement, and the pass fails when it cannot honour the row, because a silent relocation would make PIX's correlation wrong without the caller knowing. The option upstream-sv-position-row stays accepted, because PIX builds older than the rename still send it. When a caller sends both hint spellings, preferred-sv-position-row wins. The camelCase options of the debug instrumentation pass do not change. The pass raises E_FAIL for a width or a pixel count it cannot represent. A silent wrap gives the caller wrong data with no indication. Assisted-by: Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilAddPixelHitInstrumentation.cpp | 135 ++++- .../DxilDebugInstrumentation.cpp | 153 +++++- lib/DxilPIXPasses/PixPassHelpers.cpp | 277 +++++++++- lib/DxilPIXPasses/PixPassHelpers.h | 28 +- .../pix/DebugAuthoritativeSVPositionRow.hlsl | 44 ++ .../test/HLSLFileCheck/pix/DebugBasic.hlsl | 2 +- .../pix/DebugDenseVertexShaderInput.hlsl | 43 ++ .../pix/DebugEmitCorrectViewIdStatePS.hlsl | 13 +- .../HLSLFileCheck/pix/DebugVSParameters.hlsl | 6 +- .../DebugVertexShaderInputSignatureFull.hlsl | 47 ++ ...derMultiStreamSignatureIsNotRelocated.hlsl | 60 ++ .../MeshShaderSignatureIsNotRelocated.hlsl | 64 +++ .../pixelCounterLegacyRowOptionIsAHint.hlsl | 37 ++ ...pixelCounterPreferredRowOptionIsAHint.hlsl | 30 + ...lCounterPreferredRowWinsOverLegacyRow.hlsl | 29 + ...ixelCounterRelocationAtSignatureLimit.hlsl | 45 ++ ...CounterRelocationRepacksIntoSharedRow.hlsl | 34 ++ tools/clang/unittests/HLSL/PixTest.cpp | 520 ++++++++++++++++++ utils/hct/hctdb.py | 5 + 19 files changed, 1491 insertions(+), 81 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/DebugAuthoritativeSVPositionRow.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DebugDenseVertexShaderInput.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DebugVertexShaderInputSignatureFull.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/GeometryShaderMultiStreamSignatureIsNotRelocated.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/MeshShaderSignatureIsNotRelocated.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/pixelCounterLegacyRowOptionIsAHint.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowOptionIsAHint.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowWinsOverLegacyRow.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationAtSignatureLimit.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationRepacksIntoSharedRow.hlsl diff --git a/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp b/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp index 23bc224826..00f68ce379 100644 --- a/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp @@ -23,6 +23,9 @@ #include "PixPassHelpers.h" +#include "dxc/Support/Global.h" +#include + using namespace llvm; using namespace hlsl; @@ -41,7 +44,9 @@ class DxilAddPixelHitInstrumentation : public ModulePass { } void applyOptions(PassOptions O) override; bool runOnModule(Module &M) override; - unsigned m_upstreamSVPositionRow; + unsigned m_upstreamSVPositionRow = PIXPassHelpers::kUnknownSVPositionRow; + PIXPassHelpers::SVPositionRowAuthority m_svPositionRowAuthority = + PIXPassHelpers::SVPositionRowAuthority::Hint; }; void DxilAddPixelHitInstrumentation::applyOptions(PassOptions O) { @@ -49,8 +54,50 @@ void DxilAddPixelHitInstrumentation::applyOptions(PassOptions O) { GetPassOptionBool(O, "add-pixel-cost", &AddPixelCost, false); GetPassOptionInt(O, "rt-width", &RTWidth, 0); GetPassOptionInt(O, "num-pixels", &NumPixels, 0); - GetPassOptionUnsigned(O, "upstream-sv-position-row", &m_upstreamSVPositionRow, - 0); + + // RTWidth and NumPixels size the counter UAV and convert SV_Position to a + // byte offset into it. Reject a width or pixel count this pass cannot + // represent -- zero, negative, or large enough that the pixel-cost half's + // high water mark (NumPixels * 2 * 4 bytes) does not fit in 32 bits -- + // instead of emitting a shader whose offset arithmetic silently wraps. + if (RTWidth <= 0 || NumPixels <= 0 || + static_cast(NumPixels) * 2 * 4 > UINT32_MAX) { + throw ::hlsl::Exception( + E_FAIL, "PIX: the pixel-hit instrumentation was given a render " + "target width or pixel count it cannot represent."); + } + + // This option always sets a hint, never a required row: treating an + // unverified row as required could evict a real interpolant based on a + // guess. + // + // GetPassOptionUnsigned leaves the value untouched when the option is + // present but unparseable, so seed the member before the call rather than + // rely on the default argument. + // + // "upstream-sv-position-row" is the pre-rename spelling: old PIX versions + // predate this rename and still send it, so it is kept as an accepted + // alias indefinitely rather than only for a deprecation window. New + // callers should prefer "preferred-sv-position-row"; if both are + // supplied, the preferred spelling wins. + m_upstreamSVPositionRow = PIXPassHelpers::kUnknownSVPositionRow; + if (!GetPassOptionUnsigned(O, "preferred-sv-position-row", + &m_upstreamSVPositionRow, + PIXPassHelpers::kUnknownSVPositionRow)) { + GetPassOptionUnsigned(O, "upstream-sv-position-row", + &m_upstreamSVPositionRow, + PIXPassHelpers::kUnknownSVPositionRow); + } + m_svPositionRowAuthority = PIXPassHelpers::SVPositionRowAuthority::Hint; + + unsigned RequiredRow = PIXPassHelpers::kUnknownSVPositionRow; + GetPassOptionUnsigned(O, "required-sv-position-row", &RequiredRow, + PIXPassHelpers::kUnknownSVPositionRow); + if (RequiredRow != PIXPassHelpers::kUnknownSVPositionRow) { + m_upstreamSVPositionRow = RequiredRow; + m_svPositionRowAuthority = + PIXPassHelpers::SVPositionRowAuthority::Authoritative; + } } bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { @@ -66,13 +113,11 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { DM.m_ShaderFlags.SetForceEarlyDepthStencil(true); } - auto SV_Position_ID = - PIXPassHelpers::FindOrAddSV_Position(DM, m_upstreamSVPositionRow); + auto SV_Position_ID = PIXPassHelpers::FindOrAddSV_Position( + DM, m_upstreamSVPositionRow, m_svPositionRowAuthority); auto EntryPointFunction = PIXPassHelpers::GetEntryFunction(DM); - auto &EntryBlock = EntryPointFunction->getEntryBlock(); - CallInst *HandleForUAV; { IRBuilder<> Builder(dxilutil::FirstNonAllocaInsertionPt( @@ -83,18 +128,32 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { DM.ReEmitDxilResources(); } - // todo: is it a reasonable assumption that there will be a "Ret" in the entry - // block, and that these are the only points from which the shader can exit - // (except for a pixel-kill?) - auto &Instructions = EntryBlock.getInstList(); - auto It = Instructions.begin(); - while (It != Instructions.end()) { - auto ThisInstruction = It++; + // Every point where the shader completes must bump the counter. A + // straight-line shader keeps its Ret in the entry block, but a shader with + // a loop or branch ends the entry block early, so every basic block is + // scanned for a Ret. + llvm::SmallVector ReturnInstructions; + bool FunctionHasWork = false; + for (auto &ThisBlock : EntryPointFunction->getBasicBlockList()) { + for (auto &ThisInstruction : ThisBlock) { + LlvmInst_Ret Ret(&ThisInstruction); + if (Ret) { + ReturnInstructions.push_back(&ThisInstruction); + } else if (!llvm::isa(&ThisInstruction)) { + FunctionHasWork = true; + } + } + } + + bool Modified = false; + + for (auto ThisInstruction : ReturnInstructions) { LlvmInst_Ret Ret(ThisInstruction); if (Ret) { - // Check that there is at least one instruction preceding the Ret (no need - // to instrument it if there isn't) - if (ThisInstruction->getPrevNode() != nullptr) { + // A function that contains nothing but terminators has no pixel work + // worth counting. + if (FunctionHasWork) { + Modified = true; // Start adding instructions right before the Ret: IRBuilder<> Builder(ThisInstruction); @@ -110,7 +169,13 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { Constant *One32Arg = HlslOP->GetU32Const(1); Constant *One8Arg = HlslOP->GetI8Const(1); UndefValue *UndefArg = UndefValue::get(Type::getInt32Ty(Ctx)); - Constant *NumPixelsByteOffsetArg = HlslOP->GetU32Const(NumPixels * 4); + // Compute as uint32_t, not NumPixels' own int: applyOptions + // guarantees NumPixels * 2 * 4 fits in 32 bits only for unsigned + // arithmetic. The signed multiply would overflow int32 for a + // NumPixels this pass accepts, which is undefined behavior on the + // host, not just a wrapped value in the shader. + Constant *NumPixelsByteOffsetArg = + HlslOP->GetU32Const(static_cast(NumPixels) * 4u); // Step 1: Convert SV_POSITION to UINT Value *XAsInt; @@ -141,12 +206,31 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { // Step 2: Calculate pixel index Value *Index; { - Constant *RTWidthArg = HlslOP->GetI32Const(RTWidth); + Constant *RTWidthArg = + HlslOP->GetU32Const(static_cast(RTWidth)); auto YOffset = Builder.CreateMul(YAsInt, RTWidthArg, "YOffset"); auto Elementoffset = Builder.CreateAdd(XAsInt, YOffset, "ElementOffset"); - Index = Builder.CreateMul(Elementoffset, HlslOP->GetU32Const(4), - "ByteIndex"); + + // The viewport can be offset from the render target's origin, or + // smaller than the counter buffer PIX sized for it, so + // SV_Position's X and Y can land ElementOffset past the last valid + // element. Clamp the element count before scaling to a byte + // offset: applyOptions guarantees (NumPixels-1)*4 fits in uint32, + // so the clamped multiply cannot wrap. Clamping after scaling + // would let an oversized ElementOffset overflow the multiply + // first. + Function *UMinOpFunc = + HlslOP->GetOpFunc(OP::OpCode::UMin, Type::getInt32Ty(Ctx)); + Constant *UMinOpcode = + HlslOP->GetU32Const((unsigned)OP::OpCode::UMin); + Constant *LastElementArg = + HlslOP->GetU32Const(static_cast(NumPixels) - 1); + auto ClampedElementOffset = Builder.CreateCall( + UMinOpFunc, {UMinOpcode, Elementoffset, LastElementArg}, + "ClampedElementOffset"); + Index = Builder.CreateMul(ClampedElementOffset, + HlslOP->GetU32Const(4), "ByteIndex"); } // Insert the UAV increment instruction: @@ -188,7 +272,8 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { Type::getInt32Ty(Ctx)); Constant *LoadWeightOpcode = HlslOP->GetU32Const((unsigned)DXIL::OpCode::BufferLoad); - Constant *OffsetIntoUAV = HlslOP->GetU32Const(NumPixels * 2 * 4); + Constant *OffsetIntoUAV = + HlslOP->GetU32Const(static_cast(NumPixels) * 2u * 4u); auto WeightStruct = Builder.CreateCall( LoadWeight, { @@ -202,7 +287,9 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { WeightStruct, static_cast(0LL), "Weight"); } - // Step 2: Update write position ("Index") to second half of the UAV + // Step 2: Update write position ("Index") to second half of the UAV. + // Index is already clamped to the first half, so this can only land + // in the second half without a clamp of its own. auto OffsetIndex = Builder.CreateAdd(Index, NumPixelsByteOffsetArg, "OffsetByteIndex"); @@ -225,8 +312,6 @@ bool DxilAddPixelHitInstrumentation::runOnModule(Module &M) { } } - bool Modified = false; - return Modified; } diff --git a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp index d533e0c95b..3e689ae877 100644 --- a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp @@ -284,7 +284,9 @@ class DxilDebugInstrumentation : public ModulePass { unsigned m_LastInstruction = static_cast(-1); uint64_t m_UAVSize = 1024 * 1024; - unsigned m_upstreamSVPositionRow; + unsigned m_upstreamSVPositionRow = PIXPassHelpers::kUnknownSVPositionRow; + PIXPassHelpers::SVPositionRowAuthority m_svPositionRowAuthority = + PIXPassHelpers::SVPositionRowAuthority::Hint; struct PerFunctionValues { CallInst *UAVHandle = nullptr; @@ -383,14 +385,38 @@ void DxilDebugInstrumentation::applyOptions(PassOptions O) { GetPassOptionUnsigned(O, "parameter1", &m_Parameters.Parameters[1], 0); GetPassOptionUnsigned(O, "parameter2", &m_Parameters.Parameters[2], 0); GetPassOptionUInt64(O, "UAVSize", &m_UAVSize, 1024 * 1024); + // The legacy option always sets a hint, never an authoritative row, + // matching DxilAddPixelHitInstrumentation: treating an unverified row as + // authoritative could evict a real interpolant based on a guess. + // + // GetPassOptionUnsigned leaves the value untouched when the option is + // present but unparseable, so seed the member before the call rather than + // rely on the default argument. + m_upstreamSVPositionRow = PIXPassHelpers::kUnknownSVPositionRow; GetPassOptionUnsigned(O, "upstreamSVPositionRow", &m_upstreamSVPositionRow, - 0); + PIXPassHelpers::kUnknownSVPositionRow); + m_svPositionRowAuthority = PIXPassHelpers::SVPositionRowAuthority::Hint; + + unsigned AuthoritativeRow = PIXPassHelpers::kUnknownSVPositionRow; + GetPassOptionUnsigned(O, "authoritativeSVPositionRow", &AuthoritativeRow, + PIXPassHelpers::kUnknownSVPositionRow); + if (AuthoritativeRow != PIXPassHelpers::kUnknownSVPositionRow) { + m_upstreamSVPositionRow = AuthoritativeRow; + m_svPositionRowAuthority = + PIXPassHelpers::SVPositionRowAuthority::Authoritative; + } } uint32_t DxilDebugInstrumentation::UAVDumpingGroundOffset() { return static_cast(m_UAVSize / 2); } +// Returned in place of a signature element ID when the element the selection +// prolog wanted could not be made available. See +// FindOrAddVSInSignatureElementForInstanceOrVertexID. +static constexpr unsigned kUnavailableSignatureElementID = + static_cast(-1); + unsigned int GetNextEmptyRow( std::vector> const &Elements) { unsigned int Row = 0; @@ -418,12 +444,27 @@ unsigned FindOrAddVSInSignatureElementForInstanceOrVertexID( }); if (ExistingElement == InputElements.end()) { + unsigned Row = GetNextEmptyRow(InputElements); + + // A signature holds at most kMaxSignatureTotalVectors registers. Each + // vertex-shader input gets its own register (PackingKind::InputAssembler), + // so a dense enough signature has no room for this element. Appending it + // anyway would write a register past the end of the signature, which the + // validator rejects; PIX does not re-validate what it patches, so the + // driver would see the failure instead. Report the element as + // unavailable and let the caller select an invocation with whatever + // identity remains. + if (Row >= hlsl::DXIL::kMaxSignatureTotalVectors) { + return kUnavailableSignatureElementID; + } + auto AddedElement = llvm::make_unique(DXIL::SigPointKind::VSIn); - unsigned Row = GetNextEmptyRow(InputElements); + // A vertex shader input is not interpolated, so the interpolation mode has + // to be Undefined; the validator rejects anything else on a VSIn element. AddedElement->Initialize( hlsl::Semantic::Get(semanticKind)->GetName(), hlsl::CompType::getU32(), - hlsl::DXIL::InterpolationMode::Constant, 1, 1, Row, 0); + hlsl::DXIL::InterpolationMode::Undefined, 1, 1, Row, 0); AddedElement->AppendSemanticIndex(0); AddedElement->SetKind(semanticKind); AddedElement->SetUsageMask(1); @@ -454,12 +495,34 @@ DxilDebugInstrumentation::addRequiredSystemValues(BuilderContext &BC, break; case DXIL::ShaderKind::Vertex: { hlsl::DxilSignature &InputSignature = BC.DM.GetInputSignature(); + size_t const ElementCountBeforeInjection = + InputSignature.GetElements().size(); SVIndices.VertexShader.VertexId = FindOrAddVSInSignatureElementForInstanceOrVertexID( InputSignature, hlsl::DXIL::SemanticKind::VertexID); SVIndices.VertexShader.InstanceId = FindOrAddVSInSignatureElementForInstanceOrVertexID( InputSignature, hlsl::DXIL::SemanticKind::InstanceID); + // Adding an input-signature element invalidates any ViewID dependency + // table in the module. + if (InputSignature.GetElements().size() != ElementCountBeforeInjection) { + PIXPassHelpers::ClearViewIdState(BC.DM); + } + // VertexID is asked for first on purpose: when the signature has room for + // only one more element, the vertex index is the more discriminating of + // the two, because a draw always has vertices and only sometimes has more + // than one instance. + char const *Selection = "None"; + if (SVIndices.VertexShader.VertexId != kUnavailableSignatureElementID) { + Selection = + SVIndices.VertexShader.InstanceId != kUnavailableSignatureElementID + ? "VertexIdAndInstanceId" + : "VertexIdOnly"; + } else if (SVIndices.VertexShader.InstanceId != + kUnavailableSignatureElementID) { + Selection = "InstanceIdOnly"; + } + *OSOverride << "VertexShaderSelection:" << Selection << "\n"; } break; case DXIL::ShaderKind::Geometry: case DXIL::ShaderKind::Hull: @@ -468,8 +531,8 @@ DxilDebugInstrumentation::addRequiredSystemValues(BuilderContext &BC, // in the input signature break; case DXIL::ShaderKind::Pixel: { - SVIndices.PixelShader.Position = - PIXPassHelpers::FindOrAddSV_Position(BC.DM, m_upstreamSVPositionRow); + SVIndices.PixelShader.Position = PIXPassHelpers::FindOrAddSV_Position( + BC.DM, m_upstreamSVPositionRow, m_svPositionRowAuthority); } break; default: assert(false); // guaranteed by runOnModule @@ -559,33 +622,63 @@ DxilDebugInstrumentation::addVertexShaderProlog(BuilderContext &BC, BC.HlslOP->GetOpFunc(DXIL::OpCode::LoadInput, Type::getInt32Ty(BC.Ctx)); Constant *LoadInputOpcode = BC.HlslOP->GetU32Const((unsigned)DXIL::OpCode::LoadInput); - Constant *SV_Vert_ID = - BC.HlslOP->GetU32Const(SVIndices.VertexShader.VertexId); - auto VertId = - BC.Builder.CreateCall(LoadInputOpFunc, - {LoadInputOpcode, SV_Vert_ID, Zero32Arg /*row*/, - Zero8Arg /*column*/, UndefArg}, - "VertId"); - - Constant *SV_Instance_ID = - BC.HlslOP->GetU32Const(SVIndices.VertexShader.InstanceId); - auto InstanceId = - BC.Builder.CreateCall(LoadInputOpFunc, - {LoadInputOpcode, SV_Instance_ID, Zero32Arg /*row*/, - Zero8Arg /*column*/, UndefArg}, - "InstanceId"); + + // A full input signature can leave this shader without one of these system + // values. One surviving value still narrows the selection to a smaller + // set, which is an acceptable approximation; if neither value is + // available, there is nothing left to narrow with, handled below. + auto LoadSystemValue = [&](unsigned ElementID, char const *Name) -> Value * { + if (ElementID == kUnavailableSignatureElementID) { + return nullptr; + } + return BC.Builder.CreateCall( + LoadInputOpFunc, + {LoadInputOpcode, BC.HlslOP->GetU32Const(ElementID), Zero32Arg /*row*/, + Zero8Arg /*column*/, UndefArg}, + Name); + }; + + Value *VertId = LoadSystemValue(SVIndices.VertexShader.VertexId, "VertId"); + Value *InstanceId = + LoadSystemValue(SVIndices.VertexShader.InstanceId, "InstanceId"); // Compare to expected vertex ID and instance ID - auto CompareToVert = BC.Builder.CreateICmpEQ( - VertId, BC.HlslOP->GetU32Const(m_Parameters.VertexShader.VertexId), - "CompareToVertId"); - auto CompareToInstance = BC.Builder.CreateICmpEQ( - InstanceId, BC.HlslOP->GetU32Const(m_Parameters.VertexShader.InstanceId), - "CompareToInstanceId"); - auto CompareBoth = - BC.Builder.CreateAnd(CompareToVert, CompareToInstance, "CompareBoth"); + Value *CompareToVert = + VertId == nullptr + ? nullptr + : BC.Builder.CreateICmpEQ( + VertId, + BC.HlslOP->GetU32Const(m_Parameters.VertexShader.VertexId), + "CompareToVertId"); + Value *CompareToInstance = + InstanceId == nullptr + ? nullptr + : BC.Builder.CreateICmpEQ( + InstanceId, + BC.HlslOP->GetU32Const(m_Parameters.VertexShader.InstanceId), + "CompareToInstanceId"); + + if (CompareToVert != nullptr && CompareToInstance != nullptr) { + return BC.Builder.CreateAnd(CompareToVert, CompareToInstance, + "CompareBoth"); + } + if (CompareToVert != nullptr) { + return CompareToVert; + } + if (CompareToInstance != nullptr) { + return CompareToInstance; + } - return CompareBoth; + // Neither identity is available, so select none: presenting an arbitrary + // vertex's trace as the one the user asked for would be misleading, and + // this lets PIX report the shader as undebuggable instead. + // VertexShaderSelection:None already records this case. + // + // GetOpFunc materializes the loadInput declaration before either system + // value's availability is known. With neither call emitted, the + // declaration is unused, which the validator rejects. + PIXPassHelpers::EraseIfUnused(BC.DM, LoadInputOpFunc); + return BC.HlslOP->GetI1Const(0); } Value *DxilDebugInstrumentation::addHullhaderProlog(BuilderContext &BC) { diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index 1137fe6cf8..f05a02c120 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -9,11 +9,13 @@ #include "dxc/DXIL/DxilFunctionProps.h" #include "dxc/DXIL/DxilInstructions.h" +#include "dxc/DXIL/DxilMetadataHelper.h" #include "dxc/DXIL/DxilModule.h" #include "dxc/DXIL/DxilOperations.h" #include "dxc/DXIL/DxilResourceBinding.h" #include "dxc/DXIL/DxilResourceProperties.h" #include "dxc/DxilRootSignature/DxilRootSignature.h" +#include "dxc/HLSL/DxilPackSignatureElement.h" #include "dxc/HLSL/DxilSpanAllocator.h" #include "llvm/IR/IRBuilder.h" @@ -389,6 +391,18 @@ void EraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction) { } } +// A stale ViewID dependency table describes registers that do not match the +// module's current signature sizes. Clearing it removes both the module's +// cached copy and its IR metadata, so a downstream pass can recompute the +// table for the current signature. +void ClearViewIdState(hlsl::DxilModule &DM) { + DM.GetSerializedViewIdState().clear(); + if (auto *ViewIdStateMD = DM.GetModule()->getNamedMetadata( + hlsl::DxilMDHelper::kDxilViewIdStateMDName)) { + DM.GetModule()->eraseNamedMetadata(ViewIdStateMD); + } +} + // Set up a UAV with structure of a single int llvm::CallInst *CreateUAVOnceForModule(hlsl::DxilModule &DM, llvm::IRBuilder<> &Builder, @@ -501,8 +515,168 @@ void ReplaceAllUsesOfInstructionWithNewValueAndDeleteInstruction( delete Instr; } +// An authoritative row is mandatory: D3D12 matches signature elements +// between stages by register, so SV_Position on any other row fails +// pipeline creation with a linkage error. That row can already hold one of +// this shader's own elements, since pixel-shader-only system values +// (SV_IsFrontFace, SV_SampleIndex, SV_PrimitiveID without a geometry shader) +// pack after the interpolated attributes. +// +// Whatever occupies that row is safe to move: the upstream stage writes +// SV_Position there, so no upstream element shares that register, so +// nothing occupying it in this shader is linkage-bound. +// +// A hint carries no such guarantee and never displaces anything: SV_Position +// goes on a free row instead. +// +// Moving an element is metadata-only: dx.op.loadInput addresses elements by +// signature element ID and its row operand is relative to the element, so no +// instruction refers to the absolute row. +static std::vector FindElementsOccupyingSignatureRow( + std::vector> const &Elements, + unsigned int Row) { + std::vector Occupants; + for (auto const &Element : Elements) { + if (!Element->IsAllocated()) + continue; + unsigned int FirstRow = static_cast(Element->GetStartRow()); + if (Row >= FirstRow && Row < FirstRow + Element->GetRows()) + Occupants.push_back(Element.get()); + } + return Occupants; +} + +// Mirrors how the validator checks a pre-allocated element against the +// allocator: kInsufficientFreeComponents from the row check only says the row +// is partly used, which is exactly what packing two scalars into one register +// looks like, so the column check is what decides. +static bool ElementFitsAtLocation(hlsl::DxilSignatureAllocator &Allocator, + hlsl::DxilPackElement const &Element, + unsigned int Row, unsigned int Column) { + hlsl::DxilSignatureAllocator::ConflictType Conflict = + Allocator.DetectRowConflict(&Element, Row); + if (Conflict != hlsl::DxilSignatureAllocator::kNoConflict && + Conflict != hlsl::DxilSignatureAllocator::kInsufficientFreeComponents) { + return false; + } + return Allocator.DetectColConflict(&Element, Row, Column) == + hlsl::DxilSignatureAllocator::kNoConflict; +} + +// Gives Added_SV_Position a home -- TargetRow when the caller has one, +// otherwise wherever it fits -- and repacks whatever that displaces, using the +// same allocator the front end packs signatures with. +// +// A displaced element takes the first row that fits it, reusing gaps instead +// of appending past the end of the signature. DxilSignatureAllocator models +// rows, component columns, interpolation-mode and data-width compatibility, +// and the 32-register signature limit together, so no placement can exceed +// that limit. +// +// Returns false with every element left exactly where it was when the +// signature has no room, rather than emit an out-of-range register. +static bool PlaceSVPositionAndRepackDisplacedElements( + hlsl::DxilSignature &Signature, DxilSignatureElement &Added_SV_Position, + unsigned int TargetRow) { + auto const &Elements = Signature.GetElements(); + bool const UseMinPrecision = Signature.UseMinPrecision(); + + std::vector Displaced; + if (TargetRow != kUnknownSVPositionRow) + Displaced = FindElementsOccupyingSignatureRow(Elements, TargetRow); + + // The allocator takes raw pointers to these adapters and holds them across + // calls, so both vectors are sized up front and never grow afterwards. + std::vector Retained; + Retained.reserve(Elements.size()); + std::vector ToRepack; + ToRepack.reserve(Displaced.size()); + + for (auto const &Element : Elements) { + DxilSignatureElement *SignatureElement = Element.get(); + bool const Displacing = std::find(Displaced.begin(), Displaced.end(), + SignatureElement) != Displaced.end(); + // Elements the packer never places -- SV_Coverage and similar, whose + // interpretation is NotPacked -- use no register, and + // DxilSignatureAllocator asserts if handed one. A well-formed signature + // never marks such an element as allocated, so one occupying the target + // row means the signature is malformed. + if (!hlsl::DxilSignature::ShouldBeAllocated( + SignatureElement->GetInterpretation()) || + !SignatureElement->IsAllocated()) { + if (Displacing) + return false; + continue; + } + if (Displacing) { + ToRepack.emplace_back(SignatureElement, UseMinPrecision); + } else { + Retained.emplace_back(SignatureElement, UseMinPrecision); + } + } + + hlsl::DxilSignatureAllocator Allocator(hlsl::DXIL::kMaxSignatureTotalVectors, + UseMinPrecision); + + // Everything that is staying put keeps the register the front end gave it: + // those elements are paired with the upstream stage by row, so repacking them + // would break exactly the linkage this function exists to preserve. + for (hlsl::DxilPackElement &Element : Retained) { + unsigned int Row = Element.GetStartRow(); + unsigned int Column = Element.GetStartCol(); + if (!ElementFitsAtLocation(Allocator, Element, Row, Column)) { + // The signature handed to this pass already overlaps itself, so there + // is no consistent register layout to add to. Refuse rather than add + // another element on top of it. + return false; + } + Allocator.PlaceElement(&Element, Row, Column); + } + + hlsl::DxilPackElement PositionElement(&Added_SV_Position, UseMinPrecision); + if (TargetRow == kUnknownSVPositionRow) { + if (Allocator.PackNext(&PositionElement, 0, + hlsl::DXIL::kMaxSignatureTotalVectors) == 0) { + return false; + } + } else { + // SV_Position is four components wide, so it always starts at column 0 and + // owns the whole register once the occupants have been evicted. + if (!ElementFitsAtLocation(Allocator, PositionElement, TargetRow, 0)) { + return false; + } + Allocator.PlaceElement(&PositionElement, TargetRow, 0); + PositionElement.SetLocation(TargetRow, 0); + } + + // The target row must be reserved before displaced elements can be + // repacked around it. Their old locations are saved so a partial repack + // that runs out of registers can be undone. + std::vector> OriginalLocations; + OriginalLocations.reserve(ToRepack.size()); + for (hlsl::DxilPackElement &Element : ToRepack) { + OriginalLocations.emplace_back(Element.Get()->GetStartRow(), + Element.Get()->GetStartCol()); + } + + for (size_t Index = 0; Index < ToRepack.size(); ++Index) { + ToRepack[Index].ClearLocation(); + if (Allocator.PackNext(&ToRepack[Index], 0, + hlsl::DXIL::kMaxSignatureTotalVectors) == 0) { + for (size_t Undo = 0; Undo <= Index; ++Undo) { + ToRepack[Undo].Get()->SetStartRow(OriginalLocations[Undo].first); + ToRepack[Undo].Get()->SetStartCol(OriginalLocations[Undo].second); + } + return false; + } + } + + return true; +} + unsigned int FindOrAddSV_Position(hlsl::DxilModule &DM, - unsigned UpStreamSVPosRow) { + unsigned UpStreamSVPosRow, + SVPositionRowAuthority RowAuthority) { hlsl::DxilSignature &InputSignature = DM.GetInputSignature(); auto &InputElements = InputSignature.GetElements(); @@ -515,25 +689,92 @@ unsigned int FindOrAddSV_Position(hlsl::DxilModule &DM, // SV_Position, if present, has to have full mask, so we needn't worry // about the shader having selected components that don't include x or y. - // If not present, we add it. - if (Existing_SV_Position == InputElements.end()) { - unsigned int StartColumn = 0; - unsigned int RowCount = 1; - unsigned int ColumnCount = 4; - auto Added_SV_Position = - llvm::make_unique(DXIL::SigPointKind::PSIn); - Added_SV_Position->Initialize("Position", hlsl::CompType::getF32(), - hlsl::DXIL::InterpolationMode::Linear, - RowCount, ColumnCount, UpStreamSVPosRow, - StartColumn); - Added_SV_Position->AppendSemanticIndex(0); - Added_SV_Position->SetKind(hlsl::DXIL::SemanticKind::Position); - // AppendElement sets the element's ID by default - auto index = InputSignature.AppendElement(std::move(Added_SV_Position)); - return InputElements[index]->GetID(); - } else { + if (Existing_SV_Position != InputElements.end()) return Existing_SV_Position->get()->GetID(); + + constexpr unsigned int RowCount = 1; + constexpr unsigned int ColumnCount = 4; + + llvm::Function *EntryFunction = GetEntryFunction(DM); + hlsl::DXIL::ShaderKind ShaderKind = + EntryFunction != nullptr ? GetFunctionShaderKind(DM, EntryFunction) + : DM.GetShaderModel()->GetKind(); + + // Evicting an occupant is sound only for a pixel shader's input signature: + // the reasoning that the upstream stage writes SV_Position at this + // register, and so nothing else, assumes one flat register space. A mesh + // shader has two -- per-vertex and per-primitive, each numbered from zero + // and packed by different rules -- so the row says nothing about what else + // may be bound there. Mesh-to-pixel pipelines do not need the relocation + // anyway, since that pairing is matched by semantic name, not register. + // + // This only rules out the shader being instrumented here. Whether the + // upstream stage was a mesh shader is not visible from this module; the + // caller that read the upstream signature decides that by declining to + // claim the row is authoritative. + unsigned int TargetRow = kUnknownSVPositionRow; + if (UpStreamSVPosRow < hlsl::DXIL::kMaxSignatureTotalVectors) { + bool const RowIsOccupied = + !FindElementsOccupyingSignatureRow(InputElements, UpStreamSVPosRow) + .empty(); + bool const MayDisplaceOccupants = + RowAuthority == SVPositionRowAuthority::Authoritative && + ShaderKind == hlsl::DXIL::ShaderKind::Pixel; + if (!RowIsOccupied || MayDisplaceOccupants) + TargetRow = UpStreamSVPosRow; + } + + auto Added_SV_Position = + llvm::make_unique(DXIL::SigPointKind::PSIn); + // LinearNoperspective is the interpolation mode the front end gives a + // pixel shader that declares SV_Position itself, so an instrumented + // shader must match it: a driver honoring a different mode would hand the + // instrumentation perspective-divided coordinates, and PIX would silently + // attribute hits to the wrong pixel. + Added_SV_Position->Initialize( + "Position", hlsl::CompType::getF32(), + hlsl::DXIL::InterpolationMode::LinearNoperspective, RowCount, + ColumnCount); + Added_SV_Position->AppendSemanticIndex(0); + Added_SV_Position->SetKind(hlsl::DXIL::SemanticKind::Position); + + if (!PlaceSVPositionAndRepackDisplacedElements( + InputSignature, *Added_SV_Position, TargetRow)) { + // An authoritative row promises which register the upstream stage + // writes SV_Position to. Placing it elsewhere would read pixel position + // from a register nothing writes and misattribute PIX's results, so fail + // instead and let the caller drop the feature for this draw. + // + // A hint carries no such promise, so the free-row fallback is still + // usable. Emitting a register past the end of the signature is never an + // option: that is invalid DXIL, and PIX does not validate what it + // patches, so the module would reach the driver unchecked. + bool const RowWasPromised = + RowAuthority == SVPositionRowAuthority::Authoritative && + TargetRow != kUnknownSVPositionRow; + if (RowWasPromised) { + throw ::hlsl::Exception( + E_FAIL, "PIX: the shader's input signature cannot accommodate the " + "SV_Position element at the register the upstream stage " + "writes it to."); + } + if (TargetRow == kUnknownSVPositionRow || + !PlaceSVPositionAndRepackDisplacedElements( + InputSignature, *Added_SV_Position, kUnknownSVPositionRow)) { + throw ::hlsl::Exception( + E_FAIL, "PIX: the shader's input signature has no room for the " + "SV_Position element the instrumentation needs to read."); + } } + + // Adding an input-signature element invalidates any ViewID dependency + // table in the module: the table's size matches the previous element + // count. + ClearViewIdState(DM); + + // AppendElement sets the element's ID by default + auto index = InputSignature.AppendElement(std::move(Added_SV_Position)); + return InputElements[index]->GetID(); } void ForEachDynamicallyIndexedResource( diff --git a/lib/DxilPIXPasses/PixPassHelpers.h b/lib/DxilPIXPasses/PixPassHelpers.h index 3d6e24d22f..b2e0feea1f 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.h +++ b/lib/DxilPIXPasses/PixPassHelpers.h @@ -9,6 +9,7 @@ #pragma once +#include #include #include @@ -49,6 +50,10 @@ llvm::CallInst *CreateHandleForResource(hlsl::DxilModule &DM, const char *name); llvm::Function *GetEntryFunction(hlsl::DxilModule &DM); void EraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction); +// A stale ViewID dependency table describes registers that do not match the +// module's current signature sizes. Call after appending a signature +// element. +void ClearViewIdState(hlsl::DxilModule &DM); std::vector GetAllInstrumentableFunctions(hlsl::DxilModule &DM); hlsl::DXIL::ShaderKind GetFunctionShaderKind(hlsl::DxilModule &DM, @@ -81,8 +86,27 @@ ExpandedStruct ExpandStructType(llvm::LLVMContext &Ctx, llvm::Type *OriginalPayloadStructType); void ReplaceAllUsesOfInstructionWithNewValueAndDeleteInstruction( llvm::Instruction *Instr, llvm::Value *newValue, llvm::Type *newType); -unsigned int FindOrAddSV_Position(hlsl::DxilModule &DM, - unsigned UpStreamSVPosRow); +// Passed as UpStreamSVPosRow when the caller cannot determine which row the +// previous stage uses for SV_Position. See FindOrAddSV_Position. +constexpr unsigned kUnknownSVPositionRow = UINT_MAX; + +// States how much the caller of FindOrAddSV_Position knows about +// UpStreamSVPosRow. The row value alone cannot distinguish the two states, so +// the caller states its confidence explicitly. +enum class SVPositionRowAuthority { + // The row may not be genuine. SV_Position is placed there only if the row + // is free; nothing already in the signature moves. + Hint, + // The row is the register the previous stage writes SV_Position to. + // SV_Position lands there, and any occupant is repacked elsewhere. + Authoritative, +}; + +// Hint is the default: it cannot make an existing signature worse, because +// nothing already present is moved. +unsigned int FindOrAddSV_Position( + hlsl::DxilModule &DM, unsigned UpStreamSVPosRow, + SVPositionRowAuthority RowAuthority = SVPositionRowAuthority::Hint); void ForEachDynamicallyIndexedResource( hlsl::DxilModule &DM, const std::function diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugAuthoritativeSVPositionRow.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugAuthoritativeSVPositionRow.hlsl new file mode 100644 index 0000000000..7b81887f3d --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DebugAuthoritativeSVPositionRow.hlsl @@ -0,0 +1,44 @@ +// RUN: %dxc -Emain -Tps_6_0 -Od %s | %opt -S -hlsl-dxil-debug-instrumentation,authoritativeSVPositionRow=0,UAVSize=65536 -hlsl-dxilemit | %FileCheck %s -check-prefixes=AUTHORITATIVE +// RUN: %dxc -Emain -Tps_6_0 -Od %s | %opt -S -hlsl-dxil-debug-instrumentation,upstreamSVPositionRow=0,UAVSize=65536 -hlsl-dxilemit | %FileCheck %s -check-prefixes=HINT + +// The debugger needs SV_Position to identify a pixel, and injects one when the +// shader does not declare it. Which register it lands on matters: the upstream +// stage writes position to a particular register, and reading it from any other +// gives the debugger coordinates nothing wrote. +// +// PIX cannot always read the upstream signature. Some PIX builds send row 0 +// both when the previous stage genuinely uses row 0 and when the row is +// unknown. The two cannot be distinguished by value, so they are +// distinguished by option name, and this pass has to honour that distinction +// the same way DxilAddPixelHitInstrumentation does. +// +// The shader below packs TEXCOORD0 and TEXCOORD1 into register 0, so the two +// spellings have visibly different correct answers. + +// Authoritative: the caller vouches for register 0, so SV_Position must land +// there and the TEXCOORDs are repacked out of the way. Checked with -DAG +// because the signature elements are emitted in element order, which puts the +// displaced TEXCOORDs ahead of the injected SV_Position. +// Row Col +// | | +// AUTHORITATIVE-DAG: !{i32 3, !"SV_Position", i8 9, i8 3, {{.*}}, i32 0, i8 0, null} +// AUTHORITATIVE-DAG: !{i32 0, !"TEXCOORD", i8 9, i8 0, {{.*}}, i32 2, i8 0, +// AUTHORITATIVE-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, {{.*}}, i32 2, i8 2, + +// Hint: the row may have been fabricated, so nothing already in the signature +// is moved. The TEXCOORDs keep register 0 and SV_Position goes elsewhere. +// HINT-DAG: !{i32 0, !"TEXCOORD", i8 9, i8 0, {{.*}}, i32 0, i8 0, +// HINT-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, {{.*}}, i32 0, i8 2, +// HINT-NOT: !{i32 3, !"SV_Position", i8 9, i8 3, {{.*}}, i32 0, i8 0, null} + +struct PSInput +{ + float2 firstUV : TEXCOORD0; + float2 secondUV : TEXCOORD1; + float4 color : COLOR0; +}; + +float4 main(PSInput input) : SV_Target +{ + return input.color + float4(input.firstUV, input.secondUV); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugBasic.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugBasic.hlsl index b211e22959..b019b46ca3 100644 --- a/tools/clang/test/HLSLFileCheck/pix/DebugBasic.hlsl +++ b/tools/clang/test/HLSLFileCheck/pix/DebugBasic.hlsl @@ -27,7 +27,7 @@ // See DxilMDHelper::EmitSignatureElement for the meaning of these entries: // ID TypeF32 SemKin Sem-Idx-Vec interp Rows Cols Row Col // | | | | | | | | | -// CHECK: !{i32 0, !"SV_Position", i8 9, i8 3, ![[SEMIDXVEC:[0-9]*]], i8 2, i32 1, i8 4, i32 2, i8 0, null} +// CHECK: !{i32 0, !"SV_Position", i8 9, i8 3, ![[SEMIDXVEC:[0-9]*]], i8 4, i32 1, i8 4, i32 2, i8 0, null} // CHECK: ![[SEMIDXVEC]] = !{i32 0} [RootSignature("")] diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugDenseVertexShaderInput.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugDenseVertexShaderInput.hlsl new file mode 100644 index 0000000000..86f1fc9db9 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DebugDenseVertexShaderInput.hlsl @@ -0,0 +1,43 @@ +// RUN: %dxc -Emain -Tvs_6_0 %s | %opt -S -hlsl-dxil-debug-instrumentation,parameter0=1,parameter1=2 -hlsl-dxilemit | %FileCheck %s + +// The debugger identifies a vertex shader invocation by (SV_VertexID, SV_InstanceID), +// injecting whichever of the two the shader did not already declare. A vertex shader +// input signature is allocated by the input assembler, which gives every element its +// own register, and D3D allows only 32 of them. The shader below uses 31 already, so +// there is room for exactly one injected system value. +// +// The pass injects as many as fit, most discriminating first, and reports which ones +// it got so PIX knows the selection is by vertex only. Selection is then exact for a +// single-instance draw and degrades to "first matching instance" otherwise, which is +// strictly better than refusing to debug the shader. + +// CHECK: VertexShaderSelection:VertexIdOnly + +// The injected SV_VertexID is element 1 (the 31-row ATTR array is a single element). +// CHECK: %VertId = call i32 @dx.op.loadInput.i32(i32 4, i32 1, i32 0, i8 0, i32 undef) +// Nothing may be loaded in between: there is no instance id to compare against. +// CHECK-NEXT: %CompareToVertId = icmp eq i32 %VertId, 1 +// CHECK-NEXT: br i1 %CompareToVertId, label %PIXInterestingBlock, label %PIXNonInterestingBlock + +// SV_VertexID must occupy the one free register, 31, and be the last thing injected. +// See DxilMDHelper::EmitSignatureElement for the meaning of these entries: +// ID TypeU32 SemKin Sem-Idx interp Rows Cols Row Col +// | | | | | | | | | +// CHECK: = !{i32 1, !"SV_VertexID", i8 5, i8 1, ![[VIDID:[0-9]*]], i8 0, i32 1, i8 1, i32 31, i8 0, + +// CHECK-NOT: !"SV_InstanceID" + +struct DenseVertexShaderInput +{ + float4 attributes[31] : ATTR; +}; + +float4 main(DenseVertexShaderInput input) : SV_Position +{ + float4 result = 0; + [unroll] for (uint index = 0; index < 31; ++index) + { + result += input.attributes[index]; + } + return result; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugEmitCorrectViewIdStatePS.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugEmitCorrectViewIdStatePS.hlsl index 51eb86a8f9..41b90dd5a9 100644 --- a/tools/clang/test/HLSLFileCheck/pix/DebugEmitCorrectViewIdStatePS.hlsl +++ b/tools/clang/test/HLSLFileCheck/pix/DebugEmitCorrectViewIdStatePS.hlsl @@ -2,9 +2,16 @@ // CHECK: !dx.viewIdState = !{![[VIEWIDDATA:[0-9]*]]} -// The debug instrumentation will have added SV_Position to the input signature for this PS. -// If view id state is correct, then this entry should have expanded to 6 i32s (previously it would have been 4) -// CHECK: ![[VIEWIDDATA]] = !{[6 x i32] +// The debug instrumentation adds SV_Position to the input signature for this +// PS on a row of its own -- overlapping TEXCOORD's row produces a module the +// validator rejects -- so the signature spans two rows and view id state +// describes eight input components. +// +// The first two entries are the input and output component counts. The remaining +// eight are the output mask for each input component: TEXCOORD.x and .y are +// components 0 and 1, and "input.Tex.xyxy" makes them drive outputs {0,2} and +// {1,3} respectively. SV_Position's four components drive nothing. +// CHECK: ![[VIEWIDDATA]] = !{[10 x i32] [i32 8, i32 4, i32 5, i32 10, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0]} struct VS_OUTPUT { diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugVSParameters.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugVSParameters.hlsl index 4f629ee0d6..307a037284 100644 --- a/tools/clang/test/HLSLFileCheck/pix/DebugVSParameters.hlsl +++ b/tools/clang/test/HLSLFileCheck/pix/DebugVSParameters.hlsl @@ -13,12 +13,14 @@ // Check that the correct metadata was emitted for vertex id and instance id. // They should have 1 row, 1 column each. Vertex ID first at row 0, then instnce at row 1. // (With each row have the same value as the corresponding ID) +// A vertex shader input is not interpolated, so the interpolation mode has to be +// Undefined (0); the validator rejects anything else. // See DxilMDHelper::EmitSignatureElement for the meaning of these entries: // ID TypeU32 SemKin Sem-Idx interp Rows Cols Row Col // | | | | | | | | | -// CHECK: = !{i32 0, !"SV_VertexID", i8 5, i8 1, ![[VIDID:[0-9]*]], i8 1, i32 1, i8 1, i32 0, i8 0, +// CHECK: = !{i32 0, !"SV_VertexID", i8 5, i8 1, ![[VIDID:[0-9]*]], i8 0, i32 1, i8 1, i32 0, i8 0, // | | | | | | | | | -// CHECK: = !{i32 1, !"SV_InstanceID", i8 5, i8 2, ![[IID:[0-9]*]], i8 1, i32 1, i8 1, i32 1, i8 0, +// CHECK: = !{i32 1, !"SV_InstanceID", i8 5, i8 2, ![[IID:[0-9]*]], i8 0, i32 1, i8 1, i32 1, i8 0, [RootSignature("")] float4 main() : SV_Position{ return float4(0,0,0,0); diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugVertexShaderInputSignatureFull.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugVertexShaderInputSignatureFull.hlsl new file mode 100644 index 0000000000..5c14d19a63 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DebugVertexShaderInputSignatureFull.hlsl @@ -0,0 +1,47 @@ +// RUN: %dxc -Emain -Tvs_6_0 %s | %opt -S -hlsl-dxil-debug-instrumentation,parameter0=1,parameter1=2 -hlsl-dxilemit | %FileCheck %s + +// The companion to DebugDenseVertexShaderInput.hlsl, which uses 31 of the 32 +// input registers and so has room for one injected system value. This shader +// uses all 32, so neither SV_VertexID nor SV_InstanceID fits and the debugger +// has no identity at all to select an invocation by. +// +// Two things are checked below. +// +// First, the loadInput declaration must not survive unused: it is materialised +// before the pass knows whether either system value is available, and with +// neither call emitted it would be left behind as an unused external function, +// which the validator rejects with "External function 'dx.op.loadInput.i32' is +// unused". +// +// Second, the fallback selects no invocation rather than every invocation. +// Selecting every invocation would hand PIX an arbitrary vertex's trace to +// present as the one the user asked for. Selecting none is the honest answer: +// PIX reports the shader as undebuggable rather than debugging the wrong +// vertex. + +// CHECK: VertexShaderSelection:None + +// Neither identity is available, so no invocation is selected. "br i1 true" +// here would mean every vertex writes debug records. +// CHECK: br i1 false, label %PIXInterestingBlock, label %PIXNonInterestingBlock + +// The integer loadInput overload must not survive as an unused declaration. +// The shader's own attributes are float, so any .i32 loadInput at all - call +// or declare - means the orphan is back. Checked after the branch above so +// this scans the declaration block at the end of the module. +// CHECK-NOT: loadInput.i32 + +struct DenseVertexShaderInput +{ + float4 attributes[32] : ATTR; +}; + +float4 main(DenseVertexShaderInput input) : SV_Position +{ + float4 result = 0; + [unroll] for (uint index = 0; index < 32; ++index) + { + result += input.attributes[index]; + } + return result; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/GeometryShaderMultiStreamSignatureIsNotRelocated.hlsl b/tools/clang/test/HLSLFileCheck/pix/GeometryShaderMultiStreamSignatureIsNotRelocated.hlsl new file mode 100644 index 0000000000..500c1f471c --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/GeometryShaderMultiStreamSignatureIsNotRelocated.hlsl @@ -0,0 +1,60 @@ +// RUN: %dxc -Emain -Tgs_6_0 %s | %opt -S -hlsl-dxil-debug-instrumentation,UAVSize=128,parameter0=1,parameter1=2,upstreamSVPositionRow=0 | %FileCheck %s + +// A geometry shader can write several output streams, each with its own +// register space, and only one of them is rasterized. This shader deliberately +// puts SV_Position on register 0 of stream 0 and register 1 of stream 1, so +// "the row SV_Position is on" is ambiguous unless the stream is part of the +// question. +// +// Whoever reads this signature to decide where a downstream pixel shader should +// expect SV_Position has to filter on the rasterized stream; picking the first +// SV_Position in the list gets register 0 here, which is the wrong answer +// whenever stream 1 is the rasterized one. The instrumentation itself never +// relocates a geometry shader signature, and this test pins that down: all four +// elements have to come out exactly as the front end packed them, so that a +// stream-blind row query cannot be papered over by a relocation. +// +// As with MeshShaderSignatureIsNotRelocated.hlsl, this pins the end-to-end +// behaviour rather than the ShaderKind guard in FindOrAddSV_Position: the +// debug-instrumentation pass never reaches that helper for a geometry shader, +// and the checks below are on the output signature while the relocation only +// touches the input one. Removing the guard would leave this test green. + +struct FirstStreamOut +{ + float4 position : SV_Position; + float2 uv : TEXCOORD0; +}; + +struct SecondStreamOut +{ + float2 uv : TEXCOORD0; + float4 position : SV_Position; +}; + +[maxvertexcount(3)] +void main(triangle float4 input[3] : SV_Position, + inout PointStream firstStream, + inout PointStream secondStream) +{ + FirstStreamOut first = (FirstStreamOut)0; + first.position = input[0]; + first.uv = float2(1, 2); + firstStream.Append(first); + + SecondStreamOut second = (SecondStreamOut)0; + second.position = input[1]; + second.uv = float2(3, 4); + secondStream.Append(second); +} + +// The pass really did run, so the signature checks below are not vacuous. +// CHECK: call i32 @dx.op.primitiveID.i32(i32 108) + +// Stream 0: SV_Position at register 0, TEXCOORD0 at register 1. +// CHECK-DAG: !{i32 0, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 0, i8 0, {{.*}}} +// CHECK-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 1, i8 0, {{.*}}} + +// Stream 1: the same two semantics, at the opposite registers. +// CHECK-DAG: !{i32 2, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 0, {{.*}}} +// CHECK-DAG: !{i32 3, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 1, i8 0, {{.*}}} diff --git a/tools/clang/test/HLSLFileCheck/pix/MeshShaderSignatureIsNotRelocated.hlsl b/tools/clang/test/HLSLFileCheck/pix/MeshShaderSignatureIsNotRelocated.hlsl new file mode 100644 index 0000000000..7831d66cbc --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/MeshShaderSignatureIsNotRelocated.hlsl @@ -0,0 +1,64 @@ +// RUN: %dxc -Emain -Tms_6_5 %s | %opt -S -hlsl-dxil-debug-instrumentation,UAVSize=128,parameter0=10,parameter1=20,parameter2=30,upstreamSVPositionRow=0 | %FileCheck %s + +// A mesh shader is the one upstream stage whose signature the SV_Position +// relocation cannot reason about. The relocation rests on the claim that if the +// previous stage writes SV_Position at register N then it writes nothing else +// there, so anything the pixel shader has at register N is unpaired and safe to +// move. A mesh shader has two output signatures, per-vertex and per-primitive, +// each numbered from zero and packed by its own rules -- as this shader shows, +// with SV_Position at per-vertex register 0 and a per-primitive attribute also +// at register 0 -- so "register N" does not identify one thing to reason about. +// +// The instrumentation therefore leaves mesh shader signatures alone even when +// it is handed a row, and this test pins that down: the three signature +// elements have to come out exactly as the front end packed them. +// +// Note on what this does and does not cover. There are two independent reasons +// a mesh shader is unaffected: the ShaderKind guard in FindOrAddSV_Position, and +// the fact that DxilDebugInstrumentation only calls it for pixel shaders at all. +// The second alone is enough to make this test pass, and the checks below are on +// the *output* signature whereas the relocation only ever touches the *input* +// one, so deleting the ShaderKind guard would not turn this test red. It +// documents the front-end packing the guard's rationale rests on (two register +// spaces, both numbered from zero) and pins the end-to-end behaviour; it is not +// a unit test of the guard itself. + +struct VertexOut +{ + float4 position : SV_Position; + float2 uv : TEXCOORD0; +}; + +struct PrimitiveOut +{ + uint layer : TEXCOORD1; +}; + +[outputtopology("triangle")] +[numthreads(3, 1, 1)] +void main(uint threadIndex : SV_GroupIndex, + out vertices VertexOut vertices[3], + out primitives PrimitiveOut primitives[1], + out indices uint3 indices[1]) +{ + SetMeshOutputCounts(3, 1); + vertices[threadIndex].position = float4(threadIndex, 0, 0, 1); + vertices[threadIndex].uv = float2(threadIndex, 1); + if (threadIndex == 0) + { + primitives[0].layer = 7; + indices[0] = uint3(0, 1, 2); + } +} + +// The pass really did run, so the signature checks below are not vacuous. +// CHECK: %PIX_DebugUAV_Handle = call %dx.types.Handle @dx.op.createHandle +// CHECK: %ThreadIdX = call i32 @dx.op.threadId.i32(i32 93, i32 0) + +// Per-vertex outputs: SV_Position at register 0, TEXCOORD0 at register 1. +// CHECK-DAG: !{i32 0, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 0, i8 0, {{.*}}} +// CHECK-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 1, i8 0, {{.*}}} + +// Per-primitive output: a different register space, whose register 0 has +// nothing to do with the per-vertex register 0 above. +// CHECK-DAG: !{i32 0, !"TEXCOORD", i8 5, i8 0, !{{[0-9]+}}, i8 1, i32 1, i8 1, i32 0, i8 0, {{.*}}} diff --git a/tools/clang/test/HLSLFileCheck/pix/pixelCounterLegacyRowOptionIsAHint.hlsl b/tools/clang/test/HLSLFileCheck/pix/pixelCounterLegacyRowOptionIsAHint.hlsl new file mode 100644 index 0000000000..40faede3fc --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/pixelCounterLegacyRowOptionIsAHint.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16,num-pixels=64,upstream-sv-position-row=0 | %FileCheck %s + +// PIX ships dxcompiler.dll separately from the PIX executable, so an older PIX +// talking to a newer compiler is routine. Older PIX sends row 0 both when the +// upstream stage really uses row 0 and when it could not read the upstream +// signature at all, which means the value carries no promise. Acting on it +// would evict a real interpolant -- one that is still linkage-bound to whatever +// the upstream stage actually is -- on the strength of a guess, breaking the +// pipeline the relocation exists to keep working. +// +// "upstream-sv-position-row" is the pre-rename spelling of +// preferred-sv-position-row, kept as an accepted alias so older PIX builds +// keep working. Both spellings mean a hint: use the row if it happens to be +// free, never move anything to clear it. Only the required-sv-position-row +// spelling licenses eviction; see +// pixelCounterRelocationRepacksIntoSharedRow.hlsl for the same shader under +// that option. + +struct PSInput +{ + float2 firstUV : TEXCOORD0; + float2 secondUV : TEXCOORD1; + float4 color : COLOR0; +}; + +float4 main(PSInput input) : SV_Target +{ + return input.color + float4(input.firstUV, input.secondUV); +} + +// Every declared input keeps the register the front end gave it. +// CHECK-DAG: !{i32 0, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 0, {{.*}}} +// CHECK-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 2, {{.*}}} +// CHECK-DAG: !{i32 2, !"COLOR", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 4, i32 1, i8 0, {{.*}}} + +// SV_Position goes to the first register that can hold it instead. +// CHECK-DAG: !{i32 {{[0-9]+}}, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 2, i8 0, null} diff --git a/tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowOptionIsAHint.hlsl b/tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowOptionIsAHint.hlsl new file mode 100644 index 0000000000..39b846b8e4 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowOptionIsAHint.hlsl @@ -0,0 +1,30 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16,num-pixels=64,preferred-sv-position-row=0 | %FileCheck %s + +// The canonical spelling of the hint option (see +// pixelCounterLegacyRowOptionIsAHint.hlsl for its pre-rename alias). Row 0 +// carries no promise, so acting on it would evict a real interpolant on the +// strength of a guess. preferred-sv-position-row must not license eviction: +// use the row if it happens to be free, never move anything to clear it. +// Only the required-sv-position-row spelling does that; see +// pixelCounterRelocationRepacksIntoSharedRow.hlsl for the same shader under +// that option. + +struct PSInput +{ + float2 firstUV : TEXCOORD0; + float2 secondUV : TEXCOORD1; + float4 color : COLOR0; +}; + +float4 main(PSInput input) : SV_Target +{ + return input.color + float4(input.firstUV, input.secondUV); +} + +// Every declared input keeps the register the front end gave it. +// CHECK-DAG: !{i32 0, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 0, {{.*}}} +// CHECK-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 2, {{.*}}} +// CHECK-DAG: !{i32 2, !"COLOR", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 4, i32 1, i8 0, {{.*}}} + +// SV_Position goes to the first register that can hold it instead. +// CHECK-DAG: !{i32 {{[0-9]+}}, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 2, i8 0, null} diff --git a/tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowWinsOverLegacyRow.hlsl b/tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowWinsOverLegacyRow.hlsl new file mode 100644 index 0000000000..8d6815362c --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/pixelCounterPreferredRowWinsOverLegacyRow.hlsl @@ -0,0 +1,29 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16,num-pixels=64,preferred-sv-position-row=3,upstream-sv-position-row=2 | %FileCheck %s + +// When both spellings are supplied, preferred-sv-position-row must win +// deterministically over the legacy upstream-sv-position-row alias. Row 2 and +// row 3 are both free here, so if the legacy value won instead, SV_Position +// would land on row 2, not row 3; this test pins the row down to prove which +// spelling was actually read. + +struct PSInput +{ + float2 firstUV : TEXCOORD0; + float2 secondUV : TEXCOORD1; + float4 color : COLOR0; +}; + +float4 main(PSInput input) : SV_Target +{ + return input.color + float4(input.firstUV, input.secondUV); +} + +// TEXCOORD0/1 share row 0, COLOR0 occupies row 1, leaving rows 2 and 3 free. +// CHECK-DAG: !{i32 0, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 0, {{.*}}} +// CHECK-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 0, i8 2, {{.*}}} +// CHECK-DAG: !{i32 2, !"COLOR", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 4, i32 1, i8 0, {{.*}}} + +// SV_Position lands on row 3 -- the preferred-sv-position-row value -- never +// row 2, which is what the legacy upstream-sv-position-row value would have +// produced had it won instead. +// CHECK-DAG: !{i32 {{[0-9]+}}, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 3, i8 0, null} diff --git a/tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationAtSignatureLimit.hlsl b/tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationAtSignatureLimit.hlsl new file mode 100644 index 0000000000..cbeb50bb9d --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationAtSignatureLimit.hlsl @@ -0,0 +1,45 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16,num-pixels=64,required-sv-position-row=30 | %FileCheck %s + +// The pathological case for the relocation: a pixel shader that has already +// used 31 of the 32 available input registers. ATTR occupies rows 0-29 as a +// single indexed element, and the two rasterizer system values share row 30 -- +// which is where the upstream stage put SV_Position, so both of them have to +// be evicted. +// +// There is exactly one spare register left, and both evicted elements are one +// component wide, so both of them fit in it. An allocator that hands each +// evicted element a fresh row instead needs two, runs off the end of the +// register file, and emits register 32 -- a number no D3D signature can hold +// and that PIX ships straight to the driver, because it does not re-run the +// validator over the modules it patches. + +struct DensePSInput +{ + float4 attributes[30] : ATTR; + uint primitiveId : SV_PrimitiveID; + bool isFrontFace : SV_IsFrontFace; +}; + +float4 main(DensePSInput input) : SV_Target +{ + float4 accumulated = 0; + [unroll] for (uint index = 0; index < 30; ++index) + { + accumulated += input.attributes[index]; + } + + accumulated.a = input.primitiveId + (input.isFrontFace ? 1.0f : 0.0f); + return accumulated; +} + +// The 30-row array is not on the target row and must stay exactly where the +// front end packed it. +// CHECK-DAG: !{i32 0, !"ATTR", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 30, i8 4, i32 0, i8 0, {{.*}}} + +// SV_Position takes the register the upstream stage used. +// CHECK-DAG: !{i32 {{[0-9]+}}, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 30, i8 0, null} + +// Both evicted system values land in the last available register, packed into +// separate components of it rather than taking a row each. +// CHECK-DAG: !{i32 1, !"SV_PrimitiveID", i8 5, i8 10, !{{[0-9]+}}, i8 1, i32 1, i8 1, i32 31, i8 0, {{.*}}} +// CHECK-DAG: !{i32 2, !"SV_IsFrontFace", i8 5, i8 13, !{{[0-9]+}}, i8 1, i32 1, i8 1, i32 31, i8 1, {{.*}}} diff --git a/tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationRepacksIntoSharedRow.hlsl b/tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationRepacksIntoSharedRow.hlsl new file mode 100644 index 0000000000..563e229924 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/pixelCounterRelocationRepacksIntoSharedRow.hlsl @@ -0,0 +1,34 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16,num-pixels=64,required-sv-position-row=0 | %FileCheck %s + +// The instrumentation has to put SV_Position on the row the upstream stage +// used, so the two TEXCOORDs packed into that row are the ones that move. The +// question this test pins down is where they move to. +// +// Both are two components wide and were sharing a single register, so a +// correct packer puts them back into a single register rather than into two +// separate rows. On a signature already near the 32-register limit, two +// separate rows would push an element off the end of the register file. + +struct PSInput +{ + float2 firstUV : TEXCOORD0; + float2 secondUV : TEXCOORD1; + float4 color : COLOR0; +}; + +float4 main(PSInput input) : SV_Target +{ + return input.color + float4(input.firstUV, input.secondUV); +} + +// SV_Position lands on the requested row, with the noperspective +// interpolation mode the front end gives a declared SV_Position. +// CHECK-DAG: !{i32 {{[0-9]+}}, !"SV_Position", i8 9, i8 3, !{{[0-9]+}}, i8 4, i32 1, i8 4, i32 0, i8 0, null} + +// COLOR is not on the target row, so it must not have been touched. +// CHECK-DAG: !{i32 2, !"COLOR", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 4, i32 1, i8 0, {{.*}}} + +// Both evicted TEXCOORDs share row 2, in the same two-component halves they +// occupied before. Row 3 is never reached. +// CHECK-DAG: !{i32 0, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 2, i8 0, {{.*}}} +// CHECK-DAG: !{i32 1, !"TEXCOORD", i8 9, i8 0, !{{[0-9]+}}, i8 2, i32 1, i8 2, i32 2, i8 2, {{.*}}} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index faf04b4aaf..ff2ebf1308 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -188,6 +189,22 @@ class PixTest : public ::testing::Test { TEST_METHOD(DebugInstrumentation_VectorAllocaWrite_Structs) + // Tests for the pixel-hit and debug instrumentation passes' SV_Position + // signature handling. + TEST_METHOD(PixelHitInstrumentation_ReturnOutsideEntryBlock) + TEST_METHOD(PixelHitInstrumentation_SVPositionRowAlreadyOccupied) + TEST_METHOD(PixelHitInstrumentation_SVPositionRowUnknown) + TEST_METHOD(PixelHitInstrumentation_SVPositionRowOccupiedBySystemValue) + TEST_METHOD(PixelHitInstrumentation_RejectsUnrepresentableDimensions) + TEST_METHOD(PixelHitInstrumentation_ClampsCounterIndexForSmallBuffer) + TEST_METHOD( + PixelHitInstrumentation_ClampOrderingSurvivesElementOffsetOverflow) + TEST_METHOD(PixelHitInstrumentation_RejectsAuthoritativeRowWithNoRoomToEvict) + TEST_METHOD( + PixelHitInstrumentation_ClearsStaleViewIdStateAfterSignatureGrowth) + TEST_METHOD(DebugInstrumentation_ClearsStaleViewIdStateAfterVSSignatureGrowth) + TEST_METHOD(Validation_PixelHit_PixelShader) + TEST_METHOD(DebugBreakInstrumentation_Basic) TEST_METHOD(DebugBreakInstrumentation_NoDebugBreak) TEST_METHOD(DebugBreakInstrumentation_Multiple) @@ -262,6 +279,38 @@ class PixTest : public ::testing::Test { std::move(pOptimizedModule), {}, Tokenize(outputText.c_str(), "\n")}; } + // std::nullopt omits the option entirely, which is how PIX signals that it + // could not read the previous stage's signature. + PassOutput RunPixelHitPass( + IDxcBlob *dxil, int RTWidth, int NumPixels, + std::optional RequiredSVPositionRow = std::nullopt) { + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-opt-mod-passes"); + std::wstring pixelHitArg = + L"-hlsl-dxil-add-pixel-hit-instrmentation,rt-width=" + + std::to_wstring(RTWidth) + L",num-pixels=" + std::to_wstring(NumPixels); + if (RequiredSVPositionRow.has_value()) { + // The required spelling: these tests know the row because they + // choose it, which is what entitles the pass to relocate an occupant. + pixelHitArg += L",required-sv-position-row=" + + std::to_wstring(*RequiredSVPositionRow); + } + Options.push_back(pixelHitArg.c_str()); + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + std::string outputText = BlobToUtf8(pText); + + return { + std::move(pOptimizedModule), {}, Tokenize(outputText.c_str(), "\n")}; + } + PassOutput RunDebugPass(IDxcBlob *dxil, int UAVSize = 1024 * 1024) { CComPtr pOptimizer; VERIFY_SUCCEEDED( @@ -5133,3 +5182,474 @@ void main() // The store three GEPs deep still carries its alloca-register-write. VERIFY_IS_TRUE(storeAnnotated); } + +/////////////////////////////////////////////////////////////////////////////// +// Tests for the pixel-hit and debug instrumentation passes' SV_Position +// signature handling: finding the shader's return in every block, safe +// counter arithmetic, and relocating the row occupant (rather than +// SV_Position itself) when the upstream stage's row is already taken. + +// Pulls a named input-signature element's start row out of the DXIL signature +// metadata, whose fields are +// {ID, Name, ComponentType, SemanticKind, SemanticIndexes, InterpolationMode, +// Rows, Cols, StartRow, StartCol, NameValueList}. +static int FindSignatureElementStartRow(std::vector const &lines, + char const *name) { + std::string const needle = std::string("!\"") + name + "\""; + for (auto const &line : lines) { + if (line.find(needle) == std::string::npos) + continue; + auto fields = Split(line, ','); + if (fields.size() < 10) + continue; + constexpr size_t StartRowField = 8; + auto const &startRowField = fields[StartRowField]; + auto valueStart = startRowField.find("i32 "); + if (valueStart == std::string::npos) + continue; + return atoi(startRowField.c_str() + valueStart + 4); + } + return -1; +} + +// Counts the pixel-hit counter increments the instrumentation emitted. Every +// increment is an atomic add against the pass's own counter UAV, so keying off +// that handle name keeps any atomic the shader itself performs out of the +// count. +static int CountPixelHitIncrements(std::vector const &lines) { + int increments = 0; + for (auto const &line : lines) { + if (line.find("dx.op.atomicBinOp") != std::string::npos && + line.find("%PIX_CountUAV_Handle") != std::string::npos) + increments++; + } + return increments; +} + +// A pixel shader containing a loop ends its entry block in a branch, not a +// return. The pass scans every block in the function for a return +// instruction, so this shader's counter still increments once per exit point. +TEST_F(PixTest, PixelHitInstrumentation_ReturnOutsideEntryBlock) { + const char *source = R"x( +float4 main(float4 pos : SV_Position, nointerpolation uint count : COUNT) + : SV_Target +{ + float4 accumulated = 0; + [loop] for (uint index = 0; index < count; ++index) + { + accumulated += pos * index; + } + return accumulated; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + // The whole point of the shader is that its return does not live in the entry + // block, so confirm the loop really did survive into the DXIL rather than + // being flattened away. + auto uninstrumentedLines = Split(Disassemble(compiled), '\n'); + int labelCount = 0; + for (auto const &line : uninstrumentedLines) { + if (line.find("; preds = ") != std::string::npos) + labelCount++; + } + VERIFY_IS_TRUE(labelCount > 0); + + auto output = RunPixelHitPass(compiled, 16, 64); + auto lines = Split(Disassemble(output.blob), '\n'); + + // The shader has one return, however many blocks control flow crosses + // to reach it, so the instrumented module increments the counter once. + VERIFY_ARE_EQUAL(1, CountPixelHitIncrements(lines)); + + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// The pixel-hit pass has to place SV_Position on the row the upstream stage +// used, because D3D12 pairs the stages by register. When the pixel shader +// has already packed one of its own inputs at that row, appending on top of +// it leaves two elements overlapping the same register and the validator +// rejects the module. +TEST_F(PixTest, PixelHitInstrumentation_SVPositionRowAlreadyOccupied) { + const char *source = R"x( +float4 main(float4 col : COLOR) : SV_Target +{ + return col; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + // Row 0 is both COLOR's row and, here, the upstream stage's SV_Position row. + auto output = RunPixelHitPass(compiled, 16, 64, 0 /*requiredSVPositionRow*/); + auto lines = Split(Disassemble(output.blob), '\n'); + + int const colorRow = FindSignatureElementStartRow(lines, "COLOR"); + int const positionRow = FindSignatureElementStartRow(lines, "SV_Position"); + + // It has to land on the upstream row and nowhere else, because D3D12 pairs + // the stages by register and an SV_Position on any other row fails pipeline + // creation outright. So the occupant is the element that moves. + VERIFY_ARE_EQUAL(0, positionRow); + VERIFY_ARE_NOT_EQUAL(colorRow, positionRow); + + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// PIX omits the option when it cannot read the previous stage's signature. The +// pass must not relocate anything on the strength of a guessed row: the +// shader's own attributes are still linkage-bound to whatever the real upstream +// stage is, so the injected SV_Position goes on a row of its own and leaves +// them alone. +TEST_F(PixTest, PixelHitInstrumentation_SVPositionRowUnknown) { + const char *source = R"x( +float4 main(float4 col : COLOR) : SV_Target +{ + return col; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + auto output = RunPixelHitPass(compiled, 16, 64); + auto lines = Split(Disassemble(output.blob), '\n'); + + VERIFY_ARE_EQUAL(0, FindSignatureElementStartRow(lines, "COLOR")); + VERIFY_ARE_EQUAL(1, FindSignatureElementStartRow(lines, "SV_Position")); + + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// The collision that actually occurs in the wild: a pixel shader reading a +// strict subset of the vertex shader's outputs plus a system value the +// rasterizer supplies. SV_PrimitiveID needs no vertex shader counterpart, so it +// packs onto row 2 -- exactly where the vertex shader here writes SV_Position. +// +// Relocating SV_Position off row 2 desynchronises the stages and D3D12 rejects +// the pipeline with "Semantic 'SV_Position', Index '0' is defined for +// mismatched hardware registers between the output stage and input stage". +// SV_PrimitiveID has no such constraint, so it is the one that moves. +TEST_F(PixTest, PixelHitInstrumentation_SVPositionRowOccupiedBySystemValue) { + const char *source = R"x( +struct PSInput +{ + float2 uv : TEXCOORD0; + float4 color : COLOR0; + uint primitiveId : SV_PrimitiveID; +}; + +float4 main(PSInput input) : SV_Target +{ + return float4(input.color.rgb, input.uv.x + input.primitiveId); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + auto output = RunPixelHitPass(compiled, 16, 64, 2 /*requiredSVPositionRow*/); + auto lines = Split(Disassemble(output.blob), '\n'); + + VERIFY_ARE_EQUAL(2, FindSignatureElementStartRow(lines, "SV_Position")); + VERIFY_ARE_NOT_EQUAL(2, + FindSignatureElementStartRow(lines, "SV_PrimitiveID")); + VERIFY_ARE_EQUAL(0, FindSignatureElementStartRow(lines, "TEXCOORD")); + VERIFY_ARE_EQUAL(1, FindSignatureElementStartRow(lines, "COLOR")); + + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// rt-width and num-pixels size the counter UAV and convert SV_Position into a +// byte offset into it. A num-pixels large enough that its pixel-cost high +// water mark (num-pixels * 2 * 4 bytes) does not fit in 32 bits has no buffer +// layout to compute offsets against, so the pass rejects it. +TEST_F(PixTest, PixelHitInstrumentation_RejectsUnrepresentableDimensions) { + const char *source = R"x( +float4 main(float4 pos : SV_Position) : SV_Target +{ + return pos; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-opt-mod-passes"); + Options.push_back(L"-hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16," + L"num-pixels=536870912,add-pixel-cost=1"); + + CComPtr pOptimizedModule; + CComPtr pText; + HRESULT hr = pOptimizer->RunOptimizer( + compiled, Options.data(), Options.size(), &pOptimizedModule, &pText); + VERIFY_FAILED(hr); +} + +// A viewport offset from the render target's origin, or a counter buffer +// smaller than rt-width * rt-height, lets SV_Position describe a pixel +// outside the rectangle num-pixels was sized for. The counter index is +// clamped into the buffer, so the atomic add cannot land outside it. +TEST_F(PixTest, PixelHitInstrumentation_ClampsCounterIndexForSmallBuffer) { + const char *source = R"x( +float4 main(float4 pos : SV_Position) : SV_Target +{ + return pos; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + // A small render target and a small pixel count stand in for a viewport that + // does not cover the whole surface the shader was told about. + const int RTWidth = 4; + const int NumPixels = 16; + auto output = RunPixelHitPass(compiled, RTWidth, NumPixels); + auto lines = Split(Disassemble(output.blob), '\n'); + + // The clamp is a UMin (DXIL binary opcode 40) against the last valid + // element in the counter's first half, applied before the element count is + // scaled to a byte offset. + const std::string expectedClamp = + "= call i32 @dx.op.binary.i32(i32 40, i32 %ElementOffset, i32 " + + std::to_string(NumPixels - 1) + ")"; + bool foundClamp = false; + bool incrementUsesClampedIndex = false; + for (auto const &line : lines) { + if (line.find(expectedClamp) != std::string::npos) + foundClamp = true; + if (line.find("dx.op.atomicBinOp") != std::string::npos && + line.find("%PIX_CountUAV_Handle") != std::string::npos && + line.find("%ByteIndex") != std::string::npos) + incrementUsesClampedIndex = true; + } + VERIFY_IS_TRUE(foundClamp); + VERIFY_IS_TRUE(incrementUsesClampedIndex); + + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// applyOptions bounds num-pixels but not rt-width, so a caller can pass a +// rt-width that num-pixels has no room for. SV_Position's Y coordinate then +// drives the element count past num-pixels by enough that scaling it to a +// byte offset first, then clamping, lets the 32-bit multiply wrap before the +// clamp ever sees it. Clamping the element count first never wraps: +// (NumPixels-1)*4 is always in range, so the clamped count is always safe to +// scale by 4. +TEST_F(PixTest, + PixelHitInstrumentation_ClampOrderingSurvivesElementOffsetOverflow) { + // YIndex=1 alone drives ElementOffset to RTWidth, and scaling that by 4 + // wraps a 32-bit value to 0 before any clamp can bound it. + const uint32_t RTWidth = 0x40000000; + const uint32_t NumPixels = 64; + const uint32_t YIndex = 1; + const uint32_t XIndex = 0; + const uint32_t ElementOffset = XIndex + YIndex * RTWidth; + const uint32_t MaxCounterByteIndex = (NumPixels - 1) * 4; + + // Scaling first, then clamping the byte offset: the multiply wraps + // ElementOffset to 0, and UMin of 0 against the limit is still 0 -- the + // increment lands on pixel 0's slot instead of the last one. + const uint32_t byteIndexScaledFirst = ElementOffset * 4u; + const uint32_t clampedAfterScaling = + std::min(byteIndexScaledFirst, MaxCounterByteIndex); + VERIFY_ARE_EQUAL(0u, byteIndexScaledFirst); + VERIFY_ARE_EQUAL(0u, clampedAfterScaling); + + // Clamping the element count first, then scaling: the clamp can only + // shrink ElementOffset, so the multiply that follows is always within the + // range applyOptions already guarantees is safe. + const uint32_t clampedElementOffset = std::min(ElementOffset, NumPixels - 1); + const uint32_t byteIndexClampedFirst = clampedElementOffset * 4u; + VERIFY_ARE_EQUAL(MaxCounterByteIndex, byteIndexClampedFirst); + + VERIFY_ARE_NOT_EQUAL(clampedAfterScaling, byteIndexClampedFirst); + + // The pass itself clamps %ElementOffset, not a byte offset derived from it: + // confirm both the clamp's operand and the final increment's operand match + // that ordering for this exact hazard. + const char *source = R"x( +float4 main(float4 pos : SV_Position) : SV_Target +{ + return pos; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + auto output = RunPixelHitPass(compiled, static_cast(RTWidth), + static_cast(NumPixels)); + auto lines = Split(Disassemble(output.blob), '\n'); + + const std::string expectedClamp = + "= call i32 @dx.op.binary.i32(i32 40, i32 %ElementOffset, i32 " + + std::to_string(NumPixels - 1) + ")"; + bool foundClamp = false; + bool incrementUsesByteIndex = false; + for (auto const &line : lines) { + if (line.find(expectedClamp) != std::string::npos) + foundClamp = true; + if (line.find("dx.op.atomicBinOp") != std::string::npos && + line.find("%PIX_CountUAV_Handle") != std::string::npos && + line.find("%ByteIndex") != std::string::npos) + incrementUsesByteIndex = true; + } + VERIFY_IS_TRUE(foundClamp); + VERIFY_IS_TRUE(incrementUsesByteIndex); + + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// A pixel shader input signature that already fills every one of the 32 +// available registers: ATTR occupies rows 0-30, and the two rasterizer +// system values share row 31 with each other -- and with the upstream +// stage's SV_Position. Evicting them to make room for SV_Position leaves +// nowhere for either of them to go, so placing SV_Position at its +// authoritative row cannot succeed. The pass rejects the module rather than +// emitting a register past the end of the signature, and leaves both +// evicted elements at their original row instead of one of them stranded +// mid-repack. +TEST_F(PixTest, + PixelHitInstrumentation_RejectsAuthoritativeRowWithNoRoomToEvict) { + const char *source = R"x( +struct DensePSInput +{ + float4 attributes[31] : ATTR; + uint primitiveId : SV_PrimitiveID; + bool isFrontFace : SV_IsFrontFace; +}; + +float4 main(DensePSInput input) : SV_Target +{ + float4 accumulated = 0; + [unroll] for (uint index = 0; index < 31; ++index) + { + accumulated += input.attributes[index]; + } + + accumulated.a = input.primitiveId + (input.isFrontFace ? 1.0f : 0.0f); + return accumulated; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-opt-mod-passes"); + Options.push_back( + L"-hlsl-dxil-add-pixel-hit-instrmentation,rt-width=16,num-pixels=64," + L"required-sv-position-row=31"); + + CComPtr pOptimizedModule; + CComPtr pText; + HRESULT hr = pOptimizer->RunOptimizer( + compiled, Options.data(), Options.size(), &pOptimizedModule, &pText); + VERIFY_FAILED(hr); +} + +// A normal compile embeds a ViewID dependency table sized for the shader's +// declared signature. Appending SV_Position grows the signature, so the +// table must be cleared: container assembly reconciles the module's +// per-register data against the table's size, and a table sized for the +// smaller signature describes the wrong one. +TEST_F(PixTest, + PixelHitInstrumentation_ClearsStaleViewIdStateAfterSignatureGrowth) { + const char *source = R"x( +float4 main(float4 col : COLOR) : SV_Target +{ + return col; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + + // Confirm the premise: an ordinary compile of this shader really does + // embed a ViewID dependency table, so the pass has something stale to + // clear. + auto uninstrumentedLines = Split(Disassemble(compiled), '\n'); + bool hadViewIdState = false; + for (auto const &line : uninstrumentedLines) { + if (line.find("dx.viewIdState") != std::string::npos) + hadViewIdState = true; + } + VERIFY_IS_TRUE(hadViewIdState); + + auto output = RunPixelHitPass(compiled, 16, 64, 0 /*requiredSVPositionRow*/); + auto lines = Split(Disassemble(output.blob), '\n'); + + // The table describing the old, smaller signature must not survive. + bool stillHasViewIdState = false; + for (auto const &line : lines) { + if (line.find("dx.viewIdState") != std::string::npos) + stillHasViewIdState = true; + } + VERIFY_IS_FALSE(stillHasViewIdState); + + // Reassembling and validating exercises exactly this: a stale table must + // not reach container assembly. + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} + +// A normal compile embeds a ViewID dependency table sized for the shader's +// declared signature. Appending SV_VertexID or SV_InstanceID grows the +// signature, so the table must be cleared: container assembly reconciles +// the module's per-register data against the table's size, and a table +// sized for the smaller signature describes the wrong one. +TEST_F(PixTest, + DebugInstrumentation_ClearsStaleViewIdStateAfterVSSignatureGrowth) { + const char *source = R"x( +float4 main(float4 pos : POSITION) : SV_Position +{ + return pos; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"vs_6_2", {}); + + // Confirm the premise: an ordinary compile of this shader really does + // embed a ViewID dependency table, so the pass has something stale to + // clear. + auto uninstrumentedLines = Split(Disassemble(compiled), '\n'); + bool hadViewIdState = false; + for (auto const &line : uninstrumentedLines) { + if (line.find("dx.viewIdState") != std::string::npos) + hadViewIdState = true; + } + VERIFY_IS_TRUE(hadViewIdState); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-opt-mod-passes"); + Options.push_back( + L"-hlsl-dxil-debug-instrumentation,parameter0=1,parameter1=2"); + Options.push_back(L"-hlsl-dxilemit"); + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + compiled, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + auto lines = Split(Disassemble(pOptimizedModule), '\n'); + + // The table describing the old, smaller signature must not survive. + bool stillHasViewIdState = false; + for (auto const &line : lines) { + if (line.find("dx.viewIdState") != std::string::npos) + stillHasViewIdState = true; + } + VERIFY_IS_FALSE(stillHasViewIdState); + + // Reassembling and validating exercises exactly this: a stale table must + // not reach container assembly. + VerifyInstrumentedModuleIsValid(pOptimizedModule, "debug instrumentation"); +} + +// Control test for the pixel-hit pass' own use of the validation harness: a +// straightforward pixel shader, instrumented and confirmed to still validate. +TEST_F(PixTest, Validation_PixelHit_PixelShader) { + const char *source = R"x( +float4 main(float4 pos : SV_Position) : SV_Target +{ + return pos; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + auto output = RunPixelHitPass(compiled, 16, 64, 0 /*requiredSVPositionRow*/); + VerifyInstrumentedModuleIsValid(output.blob, "pixel-hit instrumentation"); +} diff --git a/utils/hct/hctdb.py b/utils/hct/hctdb.py index 8bac48b2f9..b22ec3ebfa 100644 --- a/utils/hct/hctdb.py +++ b/utils/hct/hctdb.py @@ -7050,6 +7050,10 @@ def add_pass(name, type_name, doc, opts): {"n": "add-pixel-cost", "t": "int", "c": 1}, {"n": "rt-width", "t": "int", "c": 1}, {"n": "num-pixels", "t": "int", "c": 1}, + {"n": "preferred-sv-position-row", "t": "int", "c": 1}, + {"n": "required-sv-position-row", "t": "int", "c": 1}, + # Pre-rename spelling of preferred-sv-position-row, kept so + # PIX builds older than this rename keep working. {"n": "upstream-sv-position-row", "t": "int", "c": 1}, ], ) @@ -7109,6 +7113,7 @@ def add_pass(name, type_name, doc, opts): {"n": "parameter1", "t": "int", "c": 1}, {"n": "parameter2", "t": "int", "c": 1}, {"n": "upstreamSVPositionRow", "t": "int", "c": 1}, + {"n": "authoritativeSVPositionRow", "t": "int", "c": 1}, ], ) add_pass( From fac85e19962e832e4c9c9849df21f17b430fbd21 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Wed, 2 Sep 2026 10:17:05 -0700 Subject: [PATCH 12/39] [PIX] Allow only known PIX metadata before validation Virtual-register annotation attaches metadata that DXIL does not consume, so validation of an instrumented module always reported a generic "unused metadata" diagnostic even when the module was otherwise correct. The prior handling matched that diagnostic by substring, which would also swallow a genuinely unrelated unused-metadata defect. Replace it with a structural check: on direct validation failure, clone the module, strip only the four known PIX virtual-register metadata kinds, and revalidate. Accept only if the stripped clone validates, proving PIX metadata was the sole cause. Rework Validation_ControlInvalidModuleFails so the corrupted container itself proves both facts independently: direct validation's diagnostic confirms the permitted PIX metadata is present and unused, and the harness's rejection confirms a real, non-boilerplate defect remains. Replace Validation_ControlBoilerplateOnlyFailureIsRejected, which exercised the removed string classifier, with Validation_ControlNonPixUnusedMetadataIsRejected: a foreign metadata kind alongside the module's own PIX metadata must still be rejected after the four-kind strip. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/clang/unittests/HLSL/PixTest.cpp | 234 ++++++++++++++----------- 1 file changed, 135 insertions(+), 99 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index e21546c9ac..e6c803a55c 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -164,7 +164,7 @@ class PixTest : public ::testing::Test { // (ValidateInstrumentedModule / VerifyInstrumentedModuleIsValid). TEST_METHOD(Validation_ControlValidModulePasses) TEST_METHOD(Validation_ControlInvalidModuleFails) - TEST_METHOD(Validation_ControlBoilerplateOnlyFailureIsRejected) + TEST_METHOD(Validation_ControlNonPixUnusedMetadataIsRejected) dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -303,18 +303,18 @@ class PixTest : public ::testing::Test { std::string Errors; }; - ValidationResult ValidateInstrumentedModule(IDxcBlob *pModule) { - CComPtr pContainer; - - // Some pass runners return a bare bitcode module; others already - // return a container. The validator accepts only a container. + // The validator (and the assembler, when reconstructing a container + // from bare bitcode) both require a container; some pass runners + // return bare bitcode instead. + CComPtr NormalizeToContainer(IDxcBlob *pModule) { if (hlsl::IsDxilContainerLike(pModule->GetBufferPointer(), pModule->GetBufferSize()) != nullptr) { - pContainer = pModule; - } else { - pContainer = pix_test::WrapInNewContainer(m_dllSupport, pModule); + return pModule; } + return pix_test::WrapInNewContainer(m_dllSupport, pModule); + } + ValidationResult RunValidator(IDxcBlob *pContainer) { CComPtr pValidator; VERIFY_SUCCEEDED( m_dllSupport.CreateInstance(CLSID_DxcValidator, &pValidator)); @@ -334,21 +334,82 @@ class PixTest : public ::testing::Test { return {false, BlobToUtf8(pValidationErrors)}; } - // Significant holds validator diagnostics other than boilerplate and the - // permitted metadata exception. PermittedExceptionCount counts the - // exception separately. - struct FilteredValidationDiagnostics { - std::vector Significant; - int PermittedExceptionCount = 0; - }; + // The four metadata kinds PIX's virtual-register annotation pass + // intentionally leaves unused for downstream tools to consume. See + // DxilPIXVirtualRegisters.h. + static constexpr const char *KnownPixVirtualRegisterMetadataKinds[] = { + pix_dxil::PixDxilInstNum::MDName, pix_dxil::PixDxilReg::MDName, + pix_dxil::PixAllocaReg::MDName, pix_dxil::PixAllocaRegWrite::MDName}; + + // Removes the four known PIX metadata kinds from every function and + // instruction. + static void StripKnownPixVirtualRegisterMetadata(llvm::Module &M) { + llvm::LLVMContext &Ctx = M.getContext(); + for (const char *kind : KnownPixVirtualRegisterMetadataKinds) { + unsigned kindID = Ctx.getMDKindID(kind); + for (llvm::Function &F : M) { + F.setMetadata(kindID, nullptr); + for (llvm::BasicBlock &BB : F) { + for (llvm::Instruction &I : BB) { + I.setMetadata(kindID, nullptr); + } + } + } + } + } + + // Parses pContainer into an isolated LLVM module, applies Mutate to it, + // and re-serializes into a fresh validator-ready container. + template + CComPtr CloneModuleAndMutate(IDxcBlob *pContainer, + MutatorFn Mutate) { + ModuleAndHangersOn moduleEtc(pContainer); + llvm::Module *M = moduleEtc.GetDxilModule().GetModule(); + Mutate(*M); + + llvm::SmallVector bitcode; + { + llvm::raw_svector_ostream OS(bitcode); + llvm::WriteBitcodeToFile(M, OS); + } + + CComPtr pLibrary; + VERIFY_SUCCEEDED(m_dllSupport.CreateInstance(CLSID_DxcLibrary, &pLibrary)); + CComPtr pBitcodeBlob; + VERIFY_SUCCEEDED(pLibrary->CreateBlobWithEncodingFromPinned( + bitcode.data(), static_cast(bitcode.size()), CP_ACP, + &pBitcodeBlob)); + + return pix_test::WrapInNewContainer(m_dllSupport, pBitcodeBlob); + } + + ValidationResult ValidateInstrumentedModule(IDxcBlob *pModule) { + CComPtr pContainer = NormalizeToContainer(pModule); + + ValidationResult direct = RunValidator(pContainer); + if (direct.Valid) { + return direct; + } + + // The validator's "unused metadata" diagnostic names the metadata + // node, not the kind, so text can't separate PIX's own annotations + // from any other unsupported metadata. Strip only the four known PIX + // kinds and revalidate; if that alone fixes it, PIX metadata was the + // sole cause. + CComPtr strippedContainer = + CloneModuleAndMutate(pContainer, StripKnownPixVirtualRegisterMetadata); + if (RunValidator(strippedContainer).Valid) { + return {true, {}}; + } + + return direct; + } - // Filters out boilerplate ("Validation failed.") and the permitted - // metadata exception: virtual-register annotation passes add metadata - // that DXIL does not consume, so the validator reports it as unused. Do - // not widen this filter. - FilteredValidationDiagnostics + // Joins diagnostic lines, skipping blanks and "Validation failed." + // boilerplate. + static std::string GetSignificantValidationDiagnostics(const std::string &errors) { - FilteredValidationDiagnostics result; + std::string result; std::stringstream errorStream(errors); std::string line; while (std::getline(errorStream, line)) { @@ -358,26 +419,14 @@ class PixTest : public ::testing::Test { if (line.empty() || line == "Validation failed.") { continue; } - if (line.find("All metadata must be used by dxil") != std::string::npos) { - result.PermittedExceptionCount++; - continue; - } - result.Significant.push_back(line); + result += line + "\n"; } return result; } - // True only if the diagnostics contain no significant errors and at - // least one instance of the permitted metadata exception. - bool IsPermittedValidationException( - const FilteredValidationDiagnostics &diagnostics) { - return diagnostics.Significant.empty() && - diagnostics.PermittedExceptionCount > 0; - } - - // Asserts an instrumented module validates. Accepts a module whose only - // diagnostic is the permitted metadata exception; logs and fails on any - // other validator error. + // Asserts an instrumented module validates, allowing for the four known + // PIX metadata kinds being unused; logs and fails on any other + // validator error. void VerifyInstrumentedModuleIsValid(IDxcBlob *pModule, const char *description) { ValidationResult validation = ValidateInstrumentedModule(pModule); @@ -385,23 +434,9 @@ class PixTest : public ::testing::Test { return; } - FilteredValidationDiagnostics diagnostics = - GetSignificantValidationDiagnostics(validation.Errors); - if (IsPermittedValidationException(diagnostics)) { - return; - } - - std::string joined; - if (diagnostics.Significant.empty()) { - joined = "(validator reported failure with no significant diagnostic " - "text, and no permitted metadata exception was found)"; - } else { - for (auto const &significantError : diagnostics.Significant) { - joined += significantError + "\n"; - } - } WEX::Logging::Log::Error(WEX::Common::String().Format( - L"Validation failed after %S:\n%S", description, joined.c_str())); + L"Validation failed after %S:\n%S", description, + GetSignificantValidationDiagnostics(validation.Errors).c_str())); VERIFY_FAIL(); } @@ -3626,7 +3661,8 @@ float main() : SV_Target })x"; // Virtual-register annotation adds metadata that DXIL does not consume, - // so this module only validates via the permitted metadata exception. + // so this module only validates because that metadata is one of the + // four known PIX kinds. auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); auto output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); VerifyInstrumentedModuleIsValid( @@ -3642,20 +3678,11 @@ float main() : SV_Target return 0; })x"; - // Same shader and pass as Validation_ControlValidModulePasses; only the - // corruption below differs. auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); auto output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); - // Confirm the baseline validates before corrupting it, so the failure - // below is caused by the corruption and nothing else. - VerifyInstrumentedModuleIsValid( - output.Module, - "virtual-register annotation of a trivial pixel shader, uncorrupted " - "baseline (validation harness control)"); - - // Mislabel the shader stage. The validator must reject this regardless - // of the permitted metadata exception. + // Mislabel the shader stage, so the container carries both the + // harness's permitted PIX metadata and a real defect. std::string disassembly = Disassemble(output.Module); const std::string shaderKindTag = "!\"ps\","; auto tagPosition = disassembly.find(shaderKindTag); @@ -3677,40 +3704,49 @@ float main() : SV_Target CComPtr pCorruptedContainer; VERIFY_SUCCEEDED(pAssembleResult->GetResult(&pCorruptedContainer)); + // Direct validation's own diagnostic proves the PIX metadata is present + // and otherwise unused, alongside rejecting for the mislabeled stage. + ValidationResult direct = RunValidator(pCorruptedContainer); + VERIFY_IS_FALSE(direct.Valid); + VERIFY_IS_TRUE(direct.Errors.find("All metadata must be used by dxil") != + std::string::npos); + + // The harness must still reject it, for a reason other than the + // permitted metadata. ValidationResult validation = ValidateInstrumentedModule(pCorruptedContainer); VERIFY_IS_FALSE(validation.Valid); + VERIFY_IS_FALSE( + GetSignificantValidationDiagnostics(validation.Errors).empty()); +} - // Confirm the corruption produces a real diagnostic, not just the - // permitted metadata exception. - FilteredValidationDiagnostics diagnostics = - GetSignificantValidationDiagnostics(validation.Errors); - VERIFY_IS_FALSE(diagnostics.Significant.empty()); -} - -// Tests that a validator failure is rejected unless its only diagnostic is -// the permitted metadata exception. A failure with only the "Validation -// failed." boilerplate and no exception must not pass. -TEST_F(PixTest, Validation_ControlBoilerplateOnlyFailureIsRejected) { - // Boilerplate only, no permitted exception: must be rejected. - FilteredValidationDiagnostics boilerplateOnly = - GetSignificantValidationDiagnostics("Validation failed.\n"); - VERIFY_IS_TRUE(boilerplateOnly.Significant.empty()); - VERIFY_ARE_EQUAL(boilerplateOnly.PermittedExceptionCount, 0); - VERIFY_IS_FALSE(IsPermittedValidationException(boilerplateOnly)); - - // Permitted exception present: must be accepted. - FilteredValidationDiagnostics exceptionOnly = - GetSignificantValidationDiagnostics( - "Validation failed.\n" - "All metadata must be used by dxil's users.\n"); - VERIFY_IS_TRUE(exceptionOnly.Significant.empty()); - VERIFY_IS_TRUE(exceptionOnly.PermittedExceptionCount > 0); - VERIFY_IS_TRUE(IsPermittedValidationException(exceptionOnly)); - - // Real diagnostic present: must be rejected, even with the exception. - FilteredValidationDiagnostics realDiagnostic = - GetSignificantValidationDiagnostics("Validation failed.\n" - "Some real validator diagnostic.\n"); - VERIFY_IS_FALSE(realDiagnostic.Significant.empty()); - VERIFY_IS_FALSE(IsPermittedValidationException(realDiagnostic)); +// A foreign, unused instruction metadata kind alongside the module's own +// permitted PIX metadata must still be rejected: stripping only the four +// known PIX kinds leaves it behind. +TEST_F(PixTest, Validation_ControlNonPixUnusedMetadataIsRejected) { + const char *source = R"x( +float main() : SV_Target +{ + return 0; +})x"; + + CComPtr compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + SinglePassOutput output = + RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr pContainer = NormalizeToContainer(output.Module); + + CComPtr withForeignMetadata = + CloneModuleAndMutate(pContainer, [](llvm::Module &M) { + for (llvm::Function &F : M) { + if (F.isDeclaration()) { + continue; + } + llvm::Instruction &I = *F.begin()->begin(); + I.setMetadata("not-a-pix-kind", + llvm::MDNode::get(M.getContext(), {})); + break; + } + }); + + ValidationResult validation = ValidateInstrumentedModule(withForeignMetadata); + VERIFY_IS_FALSE(validation.Valid); } From 6613667d74b5eadbefd08713e594dca5bc6cf02d Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Wed, 2 Sep 2026 17:18:09 -0700 Subject: [PATCH 13/39] [PIX] Spell explicit types in L1 validation tests Apply the repository's almost-never-auto convention to every remaining auto introduced by the L1 original and direct-feedback commits. Use the declared Compile and RunSinglePass result types and std::string::size_type for the find result. No behavior changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/clang/unittests/HLSL/PixTest.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index e6c803a55c..316aa91a09 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -3663,8 +3663,10 @@ float main() : SV_Target // Virtual-register annotation adds metadata that DXIL does not consume, // so this module only validates because that metadata is one of the // four known PIX kinds. - auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - auto output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + SinglePassOutput output = + RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); VerifyInstrumentedModuleIsValid( output.Module, "virtual-register annotation of a trivial pixel shader (validation " @@ -3678,14 +3680,16 @@ float main() : SV_Target return 0; })x"; - auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - auto output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + SinglePassOutput output = + RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); // Mislabel the shader stage, so the container carries both the // harness's permitted PIX metadata and a real defect. std::string disassembly = Disassemble(output.Module); const std::string shaderKindTag = "!\"ps\","; - auto tagPosition = disassembly.find(shaderKindTag); + std::string::size_type tagPosition = disassembly.find(shaderKindTag); VERIFY_IS_TRUE(tagPosition != std::string::npos); disassembly.replace(tagPosition, shaderKindTag.size(), "!\"vs\","); From fda0a597d1f3c1c9a0845de542cb2d8894f842b4 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Wed, 2 Sep 2026 18:19:28 -0700 Subject: [PATCH 14/39] [PIX] Remove metadata counts from comments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/clang/unittests/HLSL/PixTest.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 316aa91a09..3a046dc16c 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -334,15 +334,14 @@ class PixTest : public ::testing::Test { return {false, BlobToUtf8(pValidationErrors)}; } - // The four metadata kinds PIX's virtual-register annotation pass - // intentionally leaves unused for downstream tools to consume. See + // The metadata kinds that PIX's virtual-register annotation pass + // intentionally leaves unused for downstream tools. See // DxilPIXVirtualRegisters.h. static constexpr const char *KnownPixVirtualRegisterMetadataKinds[] = { pix_dxil::PixDxilInstNum::MDName, pix_dxil::PixDxilReg::MDName, pix_dxil::PixAllocaReg::MDName, pix_dxil::PixAllocaRegWrite::MDName}; - // Removes the four known PIX metadata kinds from every function and - // instruction. + // Removes the known PIX metadata kinds from every function and instruction. static void StripKnownPixVirtualRegisterMetadata(llvm::Module &M) { llvm::LLVMContext &Ctx = M.getContext(); for (const char *kind : KnownPixVirtualRegisterMetadataKinds) { @@ -393,9 +392,9 @@ class PixTest : public ::testing::Test { // The validator's "unused metadata" diagnostic names the metadata // node, not the kind, so text can't separate PIX's own annotations - // from any other unsupported metadata. Strip only the four known PIX - // kinds and revalidate; if that alone fixes it, PIX metadata was the - // sole cause. + // from any other unsupported metadata. Strip only the known PIX kinds + // and revalidate. If this fixes the module, PIX metadata was the only + // cause. CComPtr strippedContainer = CloneModuleAndMutate(pContainer, StripKnownPixVirtualRegisterMetadata); if (RunValidator(strippedContainer).Valid) { @@ -3723,9 +3722,8 @@ float main() : SV_Target GetSignificantValidationDiagnostics(validation.Errors).empty()); } -// A foreign, unused instruction metadata kind alongside the module's own -// permitted PIX metadata must still be rejected: stripping only the four -// known PIX kinds leaves it behind. +// A foreign, unused instruction metadata kind with the module's PIX metadata +// must still be rejected. Stripping the known PIX kinds leaves it behind. TEST_F(PixTest, Validation_ControlNonPixUnusedMetadataIsRejected) { const char *source = R"x( float main() : SV_Target From f80826f89481efaf520cf32c36dc2c1174ab3ba4 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Wed, 2 Sep 2026 18:35:23 -0700 Subject: [PATCH 15/39] [PIX] Format L1 validation changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/clang/unittests/HLSL/PixTest.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 3a046dc16c..39856eae77 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -361,7 +361,7 @@ class PixTest : public ::testing::Test { // and re-serializes into a fresh validator-ready container. template CComPtr CloneModuleAndMutate(IDxcBlob *pContainer, - MutatorFn Mutate) { + MutatorFn Mutate) { ModuleAndHangersOn moduleEtc(pContainer); llvm::Module *M = moduleEtc.GetDxilModule().GetModule(); Mutate(*M); @@ -3712,7 +3712,7 @@ float main() : SV_Target ValidationResult direct = RunValidator(pCorruptedContainer); VERIFY_IS_FALSE(direct.Valid); VERIFY_IS_TRUE(direct.Errors.find("All metadata must be used by dxil") != - std::string::npos); + std::string::npos); // The harness must still reject it, for a reason other than the // permitted metadata. @@ -3731,7 +3731,8 @@ float main() : SV_Target return 0; })x"; - CComPtr compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); SinglePassOutput output = RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); CComPtr pContainer = NormalizeToContainer(output.Module); From c3bc4656cab74fec176fd2583f0ddf6ce9bf6fdb Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Wed, 2 Sep 2026 11:34:47 -0700 Subject: [PATCH 16/39] [PIX] Set the root descriptor flags only on append ExtendRootSig returns early, without appending a parameter, when a version 1.1 root signature already holds the requested tools UAV. The caller still unconditionally cleared Descriptor.Flags on the last parameter in that case, which may be an unrelated, pre-existing parameter whose flags the caller must not disturb. Have ExtendRootSig report whether it actually appended a parameter, and only clear flags on that path. Add a regression test: a v1.1 root signature with the tools UAV already present as parameter 0 and an unrelated root descriptor as parameter 1 with DataVolatile flags. Re-requesting the same register leaves parameter 1's flags untouched; appending a genuinely new register still leaves parameter 1 untouched and gives the new parameter None flags. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/DxilPIXPasses/PixPassHelpers.cpp | 15 +++-- tools/clang/unittests/HLSL/PixTest.cpp | 78 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index 1137fe6cf8..709aa2286d 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -198,8 +198,9 @@ static std::vector SerializeRootSignatureToVector( constexpr uint32_t toolsRegisterSpace = static_cast(-2); +// Returns whether a parameter was appended. template -void ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { +bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { auto *existingParams = rootSigDesc.pParameters; for (uint32_t i = 0; i < rootSigDesc.NumParameters; ++i) { if (rootSigDesc.pParameters[i].ParameterType == @@ -209,7 +210,7 @@ void ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { rootSigDesc.pParameters[i].Descriptor.ShaderRegister == toolsUAVRegister) { // Already added - return; + return false; } } } @@ -229,6 +230,7 @@ void ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { rootSigDesc.pParameters[rootSigDesc.NumParameters].ShaderVisibility = DxilShaderVisibility::All; rootSigDesc.NumParameters++; + return true; } static std::vector @@ -243,10 +245,11 @@ AddUAVParamterToRootSignature(const void *Data, uint32_t Size, toolsUAVRegister); break; case DxilRootSignatureVersion::Version_1_1: - ExtendRootSig(rs->Desc_1_1, - toolsUAVRegister); - rs->Desc_1_1.pParameters[rs->Desc_1_1.NumParameters - 1].Descriptor.Flags = - hlsl::DxilRootDescriptorFlags::None; + if (ExtendRootSig( + rs->Desc_1_1, toolsUAVRegister)) { + rs->Desc_1_1.pParameters[rs->Desc_1_1.NumParameters - 1] + .Descriptor.Flags = hlsl::DxilRootDescriptorFlags::None; + } break; } return SerializeRootSignatureToVector(rs); diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 9e38390162..e03f5c5c0f 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -152,6 +152,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(ToolsUav_ExtendsEveryGlobalRootSignatureSubobject) TEST_METHOD(DebugInstrumentation_RawBufferShaderFlagDeclared) TEST_METHOD(ToolsUav_RootSignatureSerializationFailurePreservesSignature) + TEST_METHOD(ToolsUav_ExtendingRootSignaturePreservesUnrelatedParameterFlags) TEST_METHOD(ConstantColor_UnusedIntOverloadIsErased) TEST_METHOD(ConstantColor_NoTargetOverloadsAreErased) TEST_METHOD(RemoveDiscards_UnusedDiscardOverloadIsErased) @@ -3422,6 +3423,83 @@ void main() VERIFY_IS_TRUE(foundRootSignature); } +TEST_F(PixTest, + ToolsUav_ExtendingRootSignaturePreservesUnrelatedParameterFlags) { + const char *source = R"x( +[numthreads(1, 1, 1)] +void main() +{ +})x"; + + DxilRootParameter1 parameters[2] = {}; + parameters[0].ParameterType = DxilRootParameterType::UAV; + parameters[0].Descriptor.RegisterSpace = static_cast(-2); + parameters[0].Descriptor.ShaderRegister = 0; + parameters[0].Descriptor.Flags = DxilRootDescriptorFlags::None; + parameters[0].ShaderVisibility = DxilShaderVisibility::All; + + parameters[1].ParameterType = DxilRootParameterType::CBV; + parameters[1].Descriptor.RegisterSpace = 0; + parameters[1].Descriptor.ShaderRegister = 0; + parameters[1].Descriptor.Flags = DxilRootDescriptorFlags::DataVolatile; + parameters[1].ShaderVisibility = DxilShaderVisibility::All; + + DxilVersionedRootSignatureDesc rootSignature = {}; + rootSignature.Version = DxilRootSignatureVersion::Version_1_1; + rootSignature.Desc_1_1.NumParameters = 2; + rootSignature.Desc_1_1.pParameters = parameters; + rootSignature.Desc_1_1.Flags = DxilRootSignatureFlags::None; + + CComPtr serializedRootSignature; + CComPtr errorBlob; + SerializeRootSignature(&rootSignature, &serializedRootSignature, &errorBlob, + true); + VERIFY_IS_NOT_NULL(serializedRootSignature); + + const uint8_t *serializedData = + static_cast(serializedRootSignature->GetBufferPointer()); + std::vector originalRootSignature( + serializedData, + serializedData + serializedRootSignature->GetBufferSize()); + + CComPtr compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); + ModuleAndHangersOn moduleEtc(compiled); + DxilModule &DM = moduleEtc.GetDxilModule(); + DM.ResetSerializedRootSignature(originalRootSignature); + + PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_TestUAV0"); + + { + const std::vector &bytes = DM.GetSerializedRootSignature(); + DxilVersionedRootSignatureDesc const *afterNoOp = nullptr; + DeserializeRootSignature(bytes.data(), static_cast(bytes.size()), + &afterNoOp); + VERIFY_ARE_EQUAL(afterNoOp->Desc_1_1.NumParameters, 2u); + VERIFY_IS_TRUE(afterNoOp->Desc_1_1.pParameters[1].Descriptor.Flags == + DxilRootDescriptorFlags::DataVolatile); + DeleteRootSignature(afterNoOp); + } + + PIXPassHelpers::CreateGlobalUAVResource(DM, 1, "PIX_TestUAV1"); + + { + const std::vector &bytes = DM.GetSerializedRootSignature(); + DxilVersionedRootSignatureDesc const *afterAdd = nullptr; + DeserializeRootSignature(bytes.data(), static_cast(bytes.size()), + &afterAdd); + VERIFY_ARE_EQUAL(afterAdd->Desc_1_1.NumParameters, 3u); + VERIFY_IS_TRUE(afterAdd->Desc_1_1.pParameters[1].Descriptor.Flags == + DxilRootDescriptorFlags::DataVolatile); + VERIFY_ARE_EQUAL(afterAdd->Desc_1_1.pParameters[2].Descriptor.RegisterSpace, + static_cast(-2)); + VERIFY_ARE_EQUAL( + afterAdd->Desc_1_1.pParameters[2].Descriptor.ShaderRegister, 1u); + VERIFY_IS_TRUE(afterAdd->Desc_1_1.pParameters[2].Descriptor.Flags == + DxilRootDescriptorFlags::None); + DeleteRootSignature(afterAdd); + } +} + static bool HasUnusedDeclaration(std::vector const &lines, std::string const &functionName) { bool declared = false; From 27008ea2c4b40b4d0747c105f9b69281ef864e77 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Wed, 2 Sep 2026 19:00:57 -0700 Subject: [PATCH 17/39] [NFC] Spell out L2 local types Replace the stable, nameable auto-deduced types introduced by L2. Retain only the two DxilSubobjects MapVector ranges whose underlying pair type is implementation-dependent and obscures the loop. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/DxilPIXPasses/PixPassHelpers.cpp | 9 ++-- tools/clang/unittests/HLSL/PixTest.cpp | 72 +++++++++++++++----------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index 709aa2286d..b83f195e32 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -296,7 +296,8 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, } constexpr bool notALocalRS = false; - for (auto const &replacementRootSignature : replacementRootSignatures) { + for (const ReplacementRootSignature &replacementRootSignature : + replacementRootSignatures) { subObjects->RemoveSubobject(replacementRootSignature.Name); subObjects->CreateRootSignature( replacementRootSignature.Name, notALocalRS, @@ -312,7 +313,7 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, const char *name) { LLVMContext &Ctx = DM.GetModule()->getContext(); - for (auto const &existingUAV : DM.GetUAVs()) { + for (const std::unique_ptr &existingUAV : DM.GetUAVs()) { if (existingUAV->GetSpaceID() == toolsRegisterSpace && existingUAV->GetLowerBound() == hlslBindIndex) { return existingUAV.get(); @@ -573,9 +574,9 @@ void ForEachDynamicallyIndexedResource( auto CreateHandleFn = HlslOP->GetOpFunc(DXIL::OpCode::CreateHandle, Type::getVoidTy(Ctx)); - auto CreateHandleFromBindingFn = HlslOP->GetOpFunc( + llvm::Function *CreateHandleFromBindingFn = HlslOP->GetOpFunc( DXIL::OpCode::CreateHandleFromBinding, Type::getVoidTy(Ctx)); - auto CreateHandleFromHeapFn = HlslOP->GetOpFunc( + llvm::Function *CreateHandleFromHeapFn = HlslOP->GetOpFunc( DXIL::OpCode::CreateHandleFromHeap, Type::getVoidTy(Ctx)); struct UnusedDeclarationCleanup { diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index e03f5c5c0f..c2cefea7bc 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -692,7 +692,7 @@ bool PixTest::InitSupport() { static unsigned CountToolsUAVs(DxilModule &DM) { unsigned count = 0; - for (auto const &uav : DM.GetUAVs()) { + for (const std::unique_ptr &uav : DM.GetUAVs()) { if (uav->GetSpaceID() == static_cast(-2)) { count++; } @@ -702,7 +702,7 @@ static unsigned CountToolsUAVs(DxilModule &DM) { static int CountToolsUAVRecords(std::vector const &lines) { int count = 0; - for (auto const &line : lines) { + for (const std::string &line : lines) { if (!line.empty() && line[0] == '!' && line.find(", i32 -2, i32 ") != std::string::npos) { count++; @@ -801,7 +801,8 @@ void PixTest::VerifyGlobalRootSignaturesHaveToolsUAVs( foundRootSignatures[subObjectName] = true; } - for (const auto &foundRootSignature : foundRootSignatures) { + for (const std::map::value_type &foundRootSignature : + foundRootSignatures) { VERIFY_IS_TRUE(foundRootSignature.second); } } @@ -1116,7 +1117,8 @@ void MSMain( FindDeclarationLine(Disassemble(as), "dx.op.dispatchMesh"); VERIFY_IS_FALSE(originalDispatchMeshDeclaration.empty()); - auto asOutput = RunDxilPIXAddTidToAmplificationShaderPayloadPass(as); + CComPtr asOutput = + RunDxilPIXAddTidToAmplificationShaderPayloadPass(as); VERIFY_IS_FALSE(HasDeclarationLine(Disassemble(asOutput), originalDispatchMeshDeclaration)); @@ -1125,7 +1127,7 @@ void MSMain( FindDeclarationLine(Disassemble(ms), "dx.op.getMeshPayload"); VERIFY_IS_FALSE(originalGetMeshPayloadDeclaration.empty()); - auto msOutput = RunDxilPIXMeshShaderOutputPass(ms); + CComPtr msOutput = RunDxilPIXMeshShaderOutputPass(ms); const std::string meshDisassembly = Disassemble(msOutput); VERIFY_IS_FALSE( HasDeclarationLine(meshDisassembly, originalGetMeshPayloadDeclaration)); @@ -3222,9 +3224,10 @@ void main(uint3 tid : SV_DispatchThreadID) output.Store(4 * tid.x, tid.x); })x"; - auto compiled = Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); - auto debugOutput = RunDebugPass(compiled); - auto accessOutput = RunShaderAccessTrackingPass(debugOutput.blob); + CComPtr compiled = + Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); + PassOutput debugOutput = RunDebugPass(compiled); + PassOutput accessOutput = RunShaderAccessTrackingPass(debugOutput.blob); ModuleAndHangersOn moduleEtc(accessOutput.blob); VERIFY_ARE_EQUAL(1u, CountToolsUAVs(moduleEtc.GetDxilModule())); @@ -3254,10 +3257,10 @@ void MissTwo(inout MyPayload payload) } )x"; - auto compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); - auto output = RunDxilPIXDXRInvocationsLog(compiled); + CComPtr compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); + CComPtr output = RunDxilPIXDXRInvocationsLog(compiled); - auto lines = Tokenize(Disassemble(output), "\n"); + std::vector lines = Tokenize(Disassemble(output), "\n"); VERIFY_ARE_EQUAL(2, CountToolsUAVRecords(lines)); } @@ -3300,7 +3303,7 @@ void MyMiss(inout MyPayload payload) } )x"; - auto compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); + CComPtr compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); ModuleAndHangersOn moduleEtc(compiled); DxilModule &DM = moduleEtc.GetDxilModule(); LoadSubobjectsFromContainerIntoModule(compiled, DM); @@ -3319,16 +3322,17 @@ void main(uint threadId : SV_DispatchThreadID) { })x"; - auto compiled = Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); - auto output = RunDebugPass(compiled); - auto lines = Tokenize(Disassemble(output.blob), "\n"); + CComPtr compiled = + Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); + PassOutput output = RunDebugPass(compiled); + std::vector lines = Tokenize(Disassemble(output.blob), "\n"); constexpr uint64_t EnableRawAndStructuredBuffers = 0x10; bool foundShaderFlags = false; uint64_t shaderFlags = 0; const std::string tagPrefix = "!{i32 0, i64 "; - for (auto const &line : lines) { - const auto tagStart = line.find(tagPrefix); + for (const std::string &line : lines) { + const std::string::size_type tagStart = line.find(tagPrefix); if (tagStart == std::string::npos) { continue; } @@ -3377,13 +3381,13 @@ void main() true); VERIFY_IS_NOT_NULL(serializedRootSignature); - auto serializedData = + const uint8_t *serializedData = static_cast(serializedRootSignature->GetBufferPointer()); std::vector originalRootSignature( serializedData, serializedData + serializedRootSignature->GetBufferSize()); - auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); + CComPtr compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); ModuleAndHangersOn moduleEtc(compiled); DxilModule &DM = moduleEtc.GetDxilModule(); DM.ResetSerializedRootSignature(originalRootSignature); @@ -3503,7 +3507,7 @@ void main() static bool HasUnusedDeclaration(std::vector const &lines, std::string const &functionName) { bool declared = false; - for (auto const &line : lines) { + for (const std::string &line : lines) { if (line.find("declare") != std::string::npos && line.find(functionName) != std::string::npos) { declared = true; @@ -3555,8 +3559,10 @@ float4 main() : SV_Target return float4(1, 2, 3, 4); })x"; - auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - auto output = RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + SinglePassOutput output = + RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); VERIFY_IS_FALSE(HasUnusedDeclaration(output.Lines, "dx.op.storeOutput.i32")); VerifyInstrumentedModuleIsValid(output.Module, @@ -3570,8 +3576,10 @@ void main() { })x"; - auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {L"-Od"}); - auto output = RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); + CComPtr compiled = + Compile(m_dllSupport, source, L"cs_6_0", {L"-Od"}); + SinglePassOutput output = + RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); const std::string disassembly = Disassemble(output.Module); VerifyInstrumentedModuleIsValid( @@ -3587,8 +3595,10 @@ float4 main() : SV_Target return float4(1, 2, 3, 4); })x"; - auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - auto output = RunSinglePass(compiled, L"-hlsl-dxil-remove-discards"); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + SinglePassOutput output = + RunSinglePass(compiled, L"-hlsl-dxil-remove-discards"); VERIFY_IS_FALSE(HasUnusedDeclaration(output.Lines, "dx.op.discard")); VerifyInstrumentedModuleIsValid(output.Module, @@ -3602,7 +3612,7 @@ float4 main() : SV_Target return float4(1, 2, 3, 4); })x"; - auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); + CComPtr compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); ModuleAndHangersOn moduleEtc(compiled); DxilModule &DM = moduleEtc.GetDxilModule(); OP *HlslOP = DM.GetOP(); @@ -3634,7 +3644,8 @@ float4 main(float2 uv : TEXCOORD0) : SV_Target return textures[(uint)uv.x].Load(int3(0, 0, 0)); })x"; - auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); ModuleAndHangersOn moduleEtc(compiled); DxilModule &DM = moduleEtc.GetDxilModule(); bool visitorCalled = false; @@ -3811,7 +3822,8 @@ float4 main(float2 uv : TEXCOORD0) : SV_Target return textures[NonUniformResourceIndex(index)].Load(int3(0, 0, 0)); })x"; - auto compiled = Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); + CComPtr compiled = + Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); std::string outputText; PassOutput output = RunDxilNonUniformResourceIndexInstrumentation(compiled, outputText); @@ -4209,7 +4221,7 @@ void main() { DebugBreak(); })x"; - auto compiled = Compile(m_dllSupport, source, L"cs_6_10", {}); + CComPtr compiled = Compile(m_dllSupport, source, L"cs_6_10", {}); auto output = RunDebugBreakPass(compiled); bool foundDebugBreak = false; for (auto const &line : output.lines) { From e6e17f3c52823f1a45fe2e08330e874cbfe7bcb5 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 3 Sep 2026 09:32:05 -0700 Subject: [PATCH 18/39] [PIX] Follow LLVM naming conventions Use Capitalized names for the parameters and local variables added by the PIX validation harness. Remove Hungarian prefixes without changing older code in PixTest.cpp. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819bcd16-83fc-4619-ac1a-b12ee50a2fe8 --- tools/clang/unittests/HLSL/PixTest.cpp | 249 ++++++++++++------------- 1 file changed, 123 insertions(+), 126 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 39856eae77..bdc50e8671 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -276,23 +276,23 @@ class PixTest : public ::testing::Test { std::vector Lines; }; - SinglePassOutput RunSinglePass(IDxcBlob *dxil, LPCWSTR passOption) { - CComPtr pOptimizer; + SinglePassOutput RunSinglePass(IDxcBlob *Dxil, LPCWSTR PassOption) { + CComPtr Optimizer; VERIFY_SUCCEEDED( - m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &Optimizer)); std::vector Options; Options.push_back(L"-opt-mod-passes"); - Options.push_back(passOption); + Options.push_back(PassOption); - CComPtr pOptimizedModule; - CComPtr pText; - VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( - dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); + CComPtr OptimizedModule; + CComPtr Text; + VERIFY_SUCCEEDED(Optimizer->RunOptimizer( + Dxil, Options.data(), Options.size(), &OptimizedModule, &Text)); - SinglePassOutput ret; - ret.Module = pOptimizedModule; - ret.Lines = Tokenize(BlobToUtf8(pText).c_str(), "\n"); - return ret; + SinglePassOutput Result; + Result.Module = OptimizedModule; + Result.Lines = Tokenize(BlobToUtf8(Text).c_str(), "\n"); + return Result; } // PIX does not validate the shaders its passes instrument, so a pass @@ -306,32 +306,32 @@ class PixTest : public ::testing::Test { // The validator (and the assembler, when reconstructing a container // from bare bitcode) both require a container; some pass runners // return bare bitcode instead. - CComPtr NormalizeToContainer(IDxcBlob *pModule) { - if (hlsl::IsDxilContainerLike(pModule->GetBufferPointer(), - pModule->GetBufferSize()) != nullptr) { - return pModule; + CComPtr NormalizeToContainer(IDxcBlob *Module) { + if (hlsl::IsDxilContainerLike(Module->GetBufferPointer(), + Module->GetBufferSize()) != nullptr) { + return Module; } - return pix_test::WrapInNewContainer(m_dllSupport, pModule); + return pix_test::WrapInNewContainer(m_dllSupport, Module); } - ValidationResult RunValidator(IDxcBlob *pContainer) { - CComPtr pValidator; + ValidationResult RunValidator(IDxcBlob *Container) { + CComPtr Validator; VERIFY_SUCCEEDED( - m_dllSupport.CreateInstance(CLSID_DxcValidator, &pValidator)); + m_dllSupport.CreateInstance(CLSID_DxcValidator, &Validator)); - CComPtr pValidationResult; - VERIFY_SUCCEEDED(pValidator->Validate(pContainer, DxcValidatorFlags_Default, - &pValidationResult)); + CComPtr OperationResult; + VERIFY_SUCCEEDED(Validator->Validate(Container, DxcValidatorFlags_Default, + &OperationResult)); - HRESULT validationStatus; - VERIFY_SUCCEEDED(pValidationResult->GetStatus(&validationStatus)); - if (SUCCEEDED(validationStatus)) { + HRESULT ValidationStatus; + VERIFY_SUCCEEDED(OperationResult->GetStatus(&ValidationStatus)); + if (SUCCEEDED(ValidationStatus)) { return {true, {}}; } - CComPtr pValidationErrors; - VERIFY_SUCCEEDED(pValidationResult->GetErrorBuffer(&pValidationErrors)); - return {false, BlobToUtf8(pValidationErrors)}; + CComPtr ValidationErrors; + VERIFY_SUCCEEDED(OperationResult->GetErrorBuffer(&ValidationErrors)); + return {false, BlobToUtf8(ValidationErrors)}; } // The metadata kinds that PIX's virtual-register annotation pass @@ -344,50 +344,50 @@ class PixTest : public ::testing::Test { // Removes the known PIX metadata kinds from every function and instruction. static void StripKnownPixVirtualRegisterMetadata(llvm::Module &M) { llvm::LLVMContext &Ctx = M.getContext(); - for (const char *kind : KnownPixVirtualRegisterMetadataKinds) { - unsigned kindID = Ctx.getMDKindID(kind); + for (const char *Kind : KnownPixVirtualRegisterMetadataKinds) { + unsigned KindID = Ctx.getMDKindID(Kind); for (llvm::Function &F : M) { - F.setMetadata(kindID, nullptr); + F.setMetadata(KindID, nullptr); for (llvm::BasicBlock &BB : F) { for (llvm::Instruction &I : BB) { - I.setMetadata(kindID, nullptr); + I.setMetadata(KindID, nullptr); } } } } } - // Parses pContainer into an isolated LLVM module, applies Mutate to it, + // Parses Container into an isolated LLVM module, applies Mutate to it, // and re-serializes into a fresh validator-ready container. template - CComPtr CloneModuleAndMutate(IDxcBlob *pContainer, + CComPtr CloneModuleAndMutate(IDxcBlob *Container, MutatorFn Mutate) { - ModuleAndHangersOn moduleEtc(pContainer); - llvm::Module *M = moduleEtc.GetDxilModule().GetModule(); + ModuleAndHangersOn ModuleEtc(Container); + llvm::Module *M = ModuleEtc.GetDxilModule().GetModule(); Mutate(*M); - llvm::SmallVector bitcode; + llvm::SmallVector Bitcode; { - llvm::raw_svector_ostream OS(bitcode); + llvm::raw_svector_ostream OS(Bitcode); llvm::WriteBitcodeToFile(M, OS); } - CComPtr pLibrary; - VERIFY_SUCCEEDED(m_dllSupport.CreateInstance(CLSID_DxcLibrary, &pLibrary)); - CComPtr pBitcodeBlob; - VERIFY_SUCCEEDED(pLibrary->CreateBlobWithEncodingFromPinned( - bitcode.data(), static_cast(bitcode.size()), CP_ACP, - &pBitcodeBlob)); + CComPtr Library; + VERIFY_SUCCEEDED(m_dllSupport.CreateInstance(CLSID_DxcLibrary, &Library)); + CComPtr BitcodeBlob; + VERIFY_SUCCEEDED(Library->CreateBlobWithEncodingFromPinned( + Bitcode.data(), static_cast(Bitcode.size()), CP_ACP, + &BitcodeBlob)); - return pix_test::WrapInNewContainer(m_dllSupport, pBitcodeBlob); + return pix_test::WrapInNewContainer(m_dllSupport, BitcodeBlob); } - ValidationResult ValidateInstrumentedModule(IDxcBlob *pModule) { - CComPtr pContainer = NormalizeToContainer(pModule); + ValidationResult ValidateInstrumentedModule(IDxcBlob *Module) { + CComPtr Container = NormalizeToContainer(Module); - ValidationResult direct = RunValidator(pContainer); - if (direct.Valid) { - return direct; + ValidationResult DirectResult = RunValidator(Container); + if (DirectResult.Valid) { + return DirectResult; } // The validator's "unused metadata" diagnostic names the metadata @@ -395,47 +395,46 @@ class PixTest : public ::testing::Test { // from any other unsupported metadata. Strip only the known PIX kinds // and revalidate. If this fixes the module, PIX metadata was the only // cause. - CComPtr strippedContainer = - CloneModuleAndMutate(pContainer, StripKnownPixVirtualRegisterMetadata); - if (RunValidator(strippedContainer).Valid) { + CComPtr StrippedContainer = + CloneModuleAndMutate(Container, StripKnownPixVirtualRegisterMetadata); + if (RunValidator(StrippedContainer).Valid) { return {true, {}}; } - return direct; + return DirectResult; } // Joins diagnostic lines, skipping blanks and "Validation failed." // boilerplate. static std::string - GetSignificantValidationDiagnostics(const std::string &errors) { - std::string result; - std::stringstream errorStream(errors); - std::string line; - while (std::getline(errorStream, line)) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); + GetSignificantValidationDiagnostics(const std::string &Errors) { + std::string Result; + std::stringstream ErrorStream(Errors); + std::string Line; + while (std::getline(ErrorStream, Line)) { + if (!Line.empty() && Line.back() == '\r') { + Line.pop_back(); } - if (line.empty() || line == "Validation failed.") { + if (Line.empty() || Line == "Validation failed.") { continue; } - result += line + "\n"; + Result += Line + "\n"; } - return result; + return Result; } - // Asserts an instrumented module validates, allowing for the four known - // PIX metadata kinds being unused; logs and fails on any other - // validator error. - void VerifyInstrumentedModuleIsValid(IDxcBlob *pModule, - const char *description) { - ValidationResult validation = ValidateInstrumentedModule(pModule); - if (validation.Valid) { + // Asserts that an instrumented module is valid when known PIX metadata is + // unused. Logs and fails on any other validator error. + void VerifyInstrumentedModuleIsValid(IDxcBlob *Module, + const char *Description) { + ValidationResult Result = ValidateInstrumentedModule(Module); + if (Result.Valid) { return; } WEX::Logging::Log::Error(WEX::Common::String().Format( - L"Validation failed after %S:\n%S", description, - GetSignificantValidationDiagnostics(validation.Errors).c_str())); + L"Validation failed after %S:\n%S", Description, + GetSignificantValidationDiagnostics(Result.Errors).c_str())); VERIFY_FAIL(); } @@ -3648,97 +3647,95 @@ void main(uint3 tid : SV_DispatchThreadID) { // Control tests for the PIX pass validation harness // (ValidateInstrumentedModule / VerifyInstrumentedModuleIsValid). // -// Both tests instrument the same trivial pixel shader with the -// virtual-register annotation pass, so the valid and invalid cases are -// directly comparable. +// These tests use the same trivial pixel shader and virtual-register +// annotation pass. This keeps the valid and invalid cases comparable. TEST_F(PixTest, Validation_ControlValidModulePasses) { - const char *source = R"x( + const char *Source = R"x( float main() : SV_Target { return 0; })x"; // Virtual-register annotation adds metadata that DXIL does not consume, - // so this module only validates because that metadata is one of the - // four known PIX kinds. - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - SinglePassOutput output = - RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + // so this module validates only because the metadata kind is known to PIX. + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + SinglePassOutput Output = + RunSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); VerifyInstrumentedModuleIsValid( - output.Module, + Output.Module, "virtual-register annotation of a trivial pixel shader (validation " "harness control)"); } TEST_F(PixTest, Validation_ControlInvalidModuleFails) { - const char *source = R"x( + const char *Source = R"x( float main() : SV_Target { return 0; })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - SinglePassOutput output = - RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + SinglePassOutput Output = + RunSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); // Mislabel the shader stage, so the container carries both the // harness's permitted PIX metadata and a real defect. - std::string disassembly = Disassemble(output.Module); - const std::string shaderKindTag = "!\"ps\","; - std::string::size_type tagPosition = disassembly.find(shaderKindTag); - VERIFY_IS_TRUE(tagPosition != std::string::npos); - disassembly.replace(tagPosition, shaderKindTag.size(), "!\"vs\","); - - CComPtr pDisassemblyBlob; - CreateBlobFromText(m_dllSupport, disassembly.c_str(), &pDisassemblyBlob); - - CComPtr pAssembler; + std::string Disassembly = Disassemble(Output.Module); + const std::string ShaderKindTag = "!\"ps\","; + std::string::size_type TagPosition = Disassembly.find(ShaderKindTag); + VERIFY_IS_TRUE(TagPosition != std::string::npos); + Disassembly.replace(TagPosition, ShaderKindTag.size(), "!\"vs\","); + + CComPtr DisassemblyBlob; + CreateBlobFromText(m_dllSupport, Disassembly.c_str(), &DisassemblyBlob); + + CComPtr Assembler; + VERIFY_SUCCEEDED(m_dllSupport.CreateInstance(CLSID_DxcAssembler, &Assembler)); + CComPtr AssembleResult; VERIFY_SUCCEEDED( - m_dllSupport.CreateInstance(CLSID_DxcAssembler, &pAssembler)); - CComPtr pAssembleResult; - VERIFY_SUCCEEDED( - pAssembler->AssembleToContainer(pDisassemblyBlob, &pAssembleResult)); - HRESULT assembleStatus; - VERIFY_SUCCEEDED(pAssembleResult->GetStatus(&assembleStatus)); - VERIFY_SUCCEEDED(assembleStatus); - CComPtr pCorruptedContainer; - VERIFY_SUCCEEDED(pAssembleResult->GetResult(&pCorruptedContainer)); + Assembler->AssembleToContainer(DisassemblyBlob, &AssembleResult)); + HRESULT AssembleStatus; + VERIFY_SUCCEEDED(AssembleResult->GetStatus(&AssembleStatus)); + VERIFY_SUCCEEDED(AssembleStatus); + CComPtr CorruptedContainer; + VERIFY_SUCCEEDED(AssembleResult->GetResult(&CorruptedContainer)); // Direct validation's own diagnostic proves the PIX metadata is present // and otherwise unused, alongside rejecting for the mislabeled stage. - ValidationResult direct = RunValidator(pCorruptedContainer); - VERIFY_IS_FALSE(direct.Valid); - VERIFY_IS_TRUE(direct.Errors.find("All metadata must be used by dxil") != - std::string::npos); + ValidationResult DirectResult = RunValidator(CorruptedContainer); + VERIFY_IS_FALSE(DirectResult.Valid); + VERIFY_IS_TRUE(DirectResult.Errors.find( + "All metadata must be used by dxil") != std::string::npos); // The harness must still reject it, for a reason other than the // permitted metadata. - ValidationResult validation = ValidateInstrumentedModule(pCorruptedContainer); - VERIFY_IS_FALSE(validation.Valid); + ValidationResult HarnessResult = + ValidateInstrumentedModule(CorruptedContainer); + VERIFY_IS_FALSE(HarnessResult.Valid); VERIFY_IS_FALSE( - GetSignificantValidationDiagnostics(validation.Errors).empty()); + GetSignificantValidationDiagnostics(HarnessResult.Errors).empty()); } // A foreign, unused instruction metadata kind with the module's PIX metadata // must still be rejected. Stripping the known PIX kinds leaves it behind. TEST_F(PixTest, Validation_ControlNonPixUnusedMetadataIsRejected) { - const char *source = R"x( + const char *Source = R"x( float main() : SV_Target { return 0; })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - SinglePassOutput output = - RunSinglePass(compiled, L"-dxil-annotate-with-virtual-regs"); - CComPtr pContainer = NormalizeToContainer(output.Module); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + SinglePassOutput Output = + RunSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr Container = NormalizeToContainer(Output.Module); - CComPtr withForeignMetadata = - CloneModuleAndMutate(pContainer, [](llvm::Module &M) { + CComPtr WithForeignMetadata = + CloneModuleAndMutate(Container, [](llvm::Module &M) { for (llvm::Function &F : M) { if (F.isDeclaration()) { continue; @@ -3750,6 +3747,6 @@ float main() : SV_Target } }); - ValidationResult validation = ValidateInstrumentedModule(withForeignMetadata); - VERIFY_IS_FALSE(validation.Valid); + ValidationResult Result = ValidateInstrumentedModule(WithForeignMetadata); + VERIFY_IS_FALSE(Result.Valid); } From ca7cc662960c664c784eeb6a9b4e26713ebd57fe Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 3 Sep 2026 10:23:37 -0700 Subject: [PATCH 19/39] [PIX] Use lower camel case for helper functions Rename the PIX validation helpers to verb phrases that start with a lowercase letter, as required by the LLVM coding standards. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819bcd16-83fc-4619-ac1a-b12ee50a2fe8 --- tools/clang/unittests/HLSL/PixTest.cpp | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index bdc50e8671..440d961288 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -161,7 +161,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(NonUniformResourceIndex_Raytracing) // Control tests for the PIX pass validation harness below - // (ValidateInstrumentedModule / VerifyInstrumentedModuleIsValid). + // (validateInstrumentedModule / verifyInstrumentedModuleIsValid). TEST_METHOD(Validation_ControlValidModulePasses) TEST_METHOD(Validation_ControlInvalidModuleFails) TEST_METHOD(Validation_ControlNonPixUnusedMetadataIsRejected) @@ -276,7 +276,7 @@ class PixTest : public ::testing::Test { std::vector Lines; }; - SinglePassOutput RunSinglePass(IDxcBlob *Dxil, LPCWSTR PassOption) { + SinglePassOutput runSinglePass(IDxcBlob *Dxil, LPCWSTR PassOption) { CComPtr Optimizer; VERIFY_SUCCEEDED( m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &Optimizer)); @@ -306,7 +306,7 @@ class PixTest : public ::testing::Test { // The validator (and the assembler, when reconstructing a container // from bare bitcode) both require a container; some pass runners // return bare bitcode instead. - CComPtr NormalizeToContainer(IDxcBlob *Module) { + CComPtr normalizeToContainer(IDxcBlob *Module) { if (hlsl::IsDxilContainerLike(Module->GetBufferPointer(), Module->GetBufferSize()) != nullptr) { return Module; @@ -314,7 +314,7 @@ class PixTest : public ::testing::Test { return pix_test::WrapInNewContainer(m_dllSupport, Module); } - ValidationResult RunValidator(IDxcBlob *Container) { + ValidationResult runValidator(IDxcBlob *Container) { CComPtr Validator; VERIFY_SUCCEEDED( m_dllSupport.CreateInstance(CLSID_DxcValidator, &Validator)); @@ -342,7 +342,7 @@ class PixTest : public ::testing::Test { pix_dxil::PixAllocaReg::MDName, pix_dxil::PixAllocaRegWrite::MDName}; // Removes the known PIX metadata kinds from every function and instruction. - static void StripKnownPixVirtualRegisterMetadata(llvm::Module &M) { + static void stripKnownPixVirtualRegisterMetadata(llvm::Module &M) { llvm::LLVMContext &Ctx = M.getContext(); for (const char *Kind : KnownPixVirtualRegisterMetadataKinds) { unsigned KindID = Ctx.getMDKindID(Kind); @@ -360,7 +360,7 @@ class PixTest : public ::testing::Test { // Parses Container into an isolated LLVM module, applies Mutate to it, // and re-serializes into a fresh validator-ready container. template - CComPtr CloneModuleAndMutate(IDxcBlob *Container, + CComPtr cloneModuleAndMutate(IDxcBlob *Container, MutatorFn Mutate) { ModuleAndHangersOn ModuleEtc(Container); llvm::Module *M = ModuleEtc.GetDxilModule().GetModule(); @@ -382,10 +382,10 @@ class PixTest : public ::testing::Test { return pix_test::WrapInNewContainer(m_dllSupport, BitcodeBlob); } - ValidationResult ValidateInstrumentedModule(IDxcBlob *Module) { - CComPtr Container = NormalizeToContainer(Module); + ValidationResult validateInstrumentedModule(IDxcBlob *Module) { + CComPtr Container = normalizeToContainer(Module); - ValidationResult DirectResult = RunValidator(Container); + ValidationResult DirectResult = runValidator(Container); if (DirectResult.Valid) { return DirectResult; } @@ -396,8 +396,8 @@ class PixTest : public ::testing::Test { // and revalidate. If this fixes the module, PIX metadata was the only // cause. CComPtr StrippedContainer = - CloneModuleAndMutate(Container, StripKnownPixVirtualRegisterMetadata); - if (RunValidator(StrippedContainer).Valid) { + cloneModuleAndMutate(Container, stripKnownPixVirtualRegisterMetadata); + if (runValidator(StrippedContainer).Valid) { return {true, {}}; } @@ -407,7 +407,7 @@ class PixTest : public ::testing::Test { // Joins diagnostic lines, skipping blanks and "Validation failed." // boilerplate. static std::string - GetSignificantValidationDiagnostics(const std::string &Errors) { + getSignificantValidationDiagnostics(const std::string &Errors) { std::string Result; std::stringstream ErrorStream(Errors); std::string Line; @@ -425,16 +425,16 @@ class PixTest : public ::testing::Test { // Asserts that an instrumented module is valid when known PIX metadata is // unused. Logs and fails on any other validator error. - void VerifyInstrumentedModuleIsValid(IDxcBlob *Module, + void verifyInstrumentedModuleIsValid(IDxcBlob *Module, const char *Description) { - ValidationResult Result = ValidateInstrumentedModule(Module); + ValidationResult Result = validateInstrumentedModule(Module); if (Result.Valid) { return; } WEX::Logging::Log::Error(WEX::Common::String().Format( L"Validation failed after %S:\n%S", Description, - GetSignificantValidationDiagnostics(Result.Errors).c_str())); + getSignificantValidationDiagnostics(Result.Errors).c_str())); VERIFY_FAIL(); } @@ -3645,7 +3645,7 @@ void main(uint3 tid : SV_DispatchThreadID) { /////////////////////////////////////////////////////////////////////////////// // Control tests for the PIX pass validation harness -// (ValidateInstrumentedModule / VerifyInstrumentedModuleIsValid). +// (validateInstrumentedModule / verifyInstrumentedModuleIsValid). // // These tests use the same trivial pixel shader and virtual-register // annotation pass. This keeps the valid and invalid cases comparable. @@ -3662,8 +3662,8 @@ float main() : SV_Target CComPtr Compiled = Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); SinglePassOutput Output = - RunSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); - VerifyInstrumentedModuleIsValid( + runSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); + verifyInstrumentedModuleIsValid( Output.Module, "virtual-register annotation of a trivial pixel shader (validation " "harness control)"); @@ -3679,7 +3679,7 @@ float main() : SV_Target CComPtr Compiled = Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); SinglePassOutput Output = - RunSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); + runSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); // Mislabel the shader stage, so the container carries both the // harness's permitted PIX metadata and a real defect. @@ -3705,7 +3705,7 @@ float main() : SV_Target // Direct validation's own diagnostic proves the PIX metadata is present // and otherwise unused, alongside rejecting for the mislabeled stage. - ValidationResult DirectResult = RunValidator(CorruptedContainer); + ValidationResult DirectResult = runValidator(CorruptedContainer); VERIFY_IS_FALSE(DirectResult.Valid); VERIFY_IS_TRUE(DirectResult.Errors.find( "All metadata must be used by dxil") != std::string::npos); @@ -3713,10 +3713,10 @@ float main() : SV_Target // The harness must still reject it, for a reason other than the // permitted metadata. ValidationResult HarnessResult = - ValidateInstrumentedModule(CorruptedContainer); + validateInstrumentedModule(CorruptedContainer); VERIFY_IS_FALSE(HarnessResult.Valid); VERIFY_IS_FALSE( - GetSignificantValidationDiagnostics(HarnessResult.Errors).empty()); + getSignificantValidationDiagnostics(HarnessResult.Errors).empty()); } // A foreign, unused instruction metadata kind with the module's PIX metadata @@ -3731,11 +3731,11 @@ float main() : SV_Target CComPtr Compiled = Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); SinglePassOutput Output = - RunSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); - CComPtr Container = NormalizeToContainer(Output.Module); + runSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr Container = normalizeToContainer(Output.Module); CComPtr WithForeignMetadata = - CloneModuleAndMutate(Container, [](llvm::Module &M) { + cloneModuleAndMutate(Container, [](llvm::Module &M) { for (llvm::Function &F : M) { if (F.isDeclaration()) { continue; @@ -3747,6 +3747,6 @@ float main() : SV_Target } }); - ValidationResult Result = ValidateInstrumentedModule(WithForeignMetadata); + ValidationResult Result = validateInstrumentedModule(WithForeignMetadata); VERIFY_IS_FALSE(Result.Valid); } From d1257a5fa5b0b988199564c320c2055360d81b4b Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 3 Sep 2026 10:44:14 -0700 Subject: [PATCH 20/39] [PIX] Apply LLVM naming to L2 additions Rename only the C++ declarations introduced by L2 and their required references. Preserve framework test names, established LLVM abbreviations, pre-existing identifiers, and embedded HLSL fixture content. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DxilDebugBreakInstrumentation.cpp | 4 +- ...NonUniformResourceIndexInstrumentation.cpp | 4 +- .../DxilOutputColorBecomesConstant.cpp | 8 +- ...lPIXAddTidToAmplificationShaderPayload.cpp | 2 +- ...DxilPIXMeshShaderOutputInstrumentation.cpp | 4 +- lib/DxilPIXPasses/DxilRemoveDiscards.cpp | 2 +- lib/DxilPIXPasses/PixPassHelpers.cpp | 61 +- lib/DxilPIXPasses/PixPassHelpers.h | 2 +- tools/clang/unittests/HLSL/PixTest.cpp | 670 +++++++++--------- 9 files changed, 379 insertions(+), 378 deletions(-) diff --git a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp index dc27f4b469..13cbf7c6c8 100644 --- a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp @@ -120,8 +120,8 @@ bool DxilDebugBreakInstrumentation::runOnModule(Module &M) { CI->eraseFromParent(); } - PIXPassHelpers::EraseIfUnused(DM, DebugBreakFunc); - PIXPassHelpers::EraseIfUnused(DM, AtomicOpFunc); + PIXPassHelpers::eraseIfUnused(DM, DebugBreakFunc); + PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); const bool modified = (PixUAVResource != nullptr); diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index 3c7a542648..cbeb778c41 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -149,8 +149,8 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { const bool modified = (PixUAVResource != nullptr); - PIXPassHelpers::EraseIfUnused(DM, WaveActiveAllEqualFunc); - PIXPassHelpers::EraseIfUnused(DM, AtomicOpFunc); + PIXPassHelpers::eraseIfUnused(DM, WaveActiveAllEqualFunc); + PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); if (modified) { DM.ReEmitDxilResources(); diff --git a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp index 227cc890a7..ff389f9348 100644 --- a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp +++ b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp @@ -125,8 +125,8 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { [&hasIntOutputs](CallInst *) { hasIntOutputs = true; }); if (!hasFloatOutputs && !hasIntOutputs) { - PIXPassHelpers::EraseIfUnused(DM, FloatOutputFunction); - PIXPassHelpers::EraseIfUnused(DM, IntOutputFunction); + PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); + PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); return false; } @@ -253,8 +253,8 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { }); } - PIXPassHelpers::EraseIfUnused(DM, FloatOutputFunction); - PIXPassHelpers::EraseIfUnused(DM, IntOutputFunction); + PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); + PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); return Modified; } diff --git a/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp b/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp index c459d4e768..10122f748f 100644 --- a/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp +++ b/lib/DxilPIXPasses/DxilPIXAddTidToAmplificationShaderPayload.cpp @@ -187,7 +187,7 @@ bool DxilPIXAddTidToAmplificationShaderPayload::runOnModule(Module &M) { cast(&*I)->getCalledFunction(); I->removeFromParent(); delete &*I; - PIXPassHelpers::EraseIfUnused(DM, OriginalDispatchMeshFn); + PIXPassHelpers::eraseIfUnused(DM, OriginalDispatchMeshFn); // Validation requires exactly one DispatchMesh in an AS, so we can exit // after the first one: DM.ReEmitDxilResources(); diff --git a/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp b/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp index 418c285576..de04a246e1 100644 --- a/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp @@ -328,7 +328,7 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { ReplaceAllUsesOfInstructionWithNewValueAndDeleteInstruction( getMeshPayloadInstructions, payload, expanded.ExpandedPayloadStructType); - PIXPassHelpers::EraseIfUnused(DM, OriginalGetMeshPayloadFunction); + PIXPassHelpers::eraseIfUnused(DM, OriginalGetMeshPayloadFunction); } } @@ -425,7 +425,7 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { } for (Function *StoreVertexOutputFunction : StoreVertexOutputFunctions) { - PIXPassHelpers::EraseIfUnused(DM, StoreVertexOutputFunction); + PIXPassHelpers::eraseIfUnused(DM, StoreVertexOutputFunction); } DM.ReEmitDxilResources(); diff --git a/lib/DxilPIXPasses/DxilRemoveDiscards.cpp b/lib/DxilPIXPasses/DxilRemoveDiscards.cpp index 72b2bf88cd..492dfebdb4 100644 --- a/lib/DxilPIXPasses/DxilRemoveDiscards.cpp +++ b/lib/DxilPIXPasses/DxilRemoveDiscards.cpp @@ -55,7 +55,7 @@ bool DxilRemoveDiscards::runOnModule(Module &M) { Modified = true; } - PIXPassHelpers::EraseIfUnused(DM, DiscardFunction); + PIXPassHelpers::eraseIfUnused(DM, DiscardFunction); return Modified; } diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index b83f195e32..2c572e4cf4 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -200,7 +200,7 @@ constexpr uint32_t toolsRegisterSpace = static_cast(-2); // Returns whether a parameter was appended. template -bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { +bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t ToolsUAVRegister) { auto *existingParams = rootSigDesc.pParameters; for (uint32_t i = 0; i < rootSigDesc.NumParameters; ++i) { if (rootSigDesc.pParameters[i].ParameterType == @@ -208,7 +208,7 @@ bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { if (rootSigDesc.pParameters[i].Descriptor.RegisterSpace == toolsRegisterSpace && rootSigDesc.pParameters[i].Descriptor.ShaderRegister == - toolsUAVRegister) { + ToolsUAVRegister) { // Already added return false; } @@ -226,7 +226,7 @@ bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { rootSigDesc.pParameters[rootSigDesc.NumParameters].Descriptor.RegisterSpace = toolsRegisterSpace; rootSigDesc.pParameters[rootSigDesc.NumParameters].Descriptor.ShaderRegister = - toolsUAVRegister; + ToolsUAVRegister; rootSigDesc.pParameters[rootSigDesc.NumParameters].ShaderVisibility = DxilShaderVisibility::All; rootSigDesc.NumParameters++; @@ -235,18 +235,18 @@ bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t toolsUAVRegister) { static std::vector AddUAVParamterToRootSignature(const void *Data, uint32_t Size, - uint32_t toolsUAVRegister) { + uint32_t ToolsUAVRegister) { DxilVersionedRootSignature rootSignature; DeserializeRootSignature(Data, Size, rootSignature.get_address_of()); auto *rs = rootSignature.get_mutable(); switch (rootSignature->Version) { case DxilRootSignatureVersion::Version_1_0: ExtendRootSig(rs->Desc_1_0, - toolsUAVRegister); + ToolsUAVRegister); break; case DxilRootSignatureVersion::Version_1_1: if (ExtendRootSig( - rs->Desc_1_1, toolsUAVRegister)) { + rs->Desc_1_1, ToolsUAVRegister)) { rs->Desc_1_1.pParameters[rs->Desc_1_1.NumParameters - 1] .Descriptor.Flags = hlsl::DxilRootDescriptorFlags::None; } @@ -256,11 +256,11 @@ AddUAVParamterToRootSignature(const void *Data, uint32_t Size, } static void AddUAVToShaderAttributeRootSignature(DxilModule &DM, - uint32_t toolsUAVRegister) { + uint32_t ToolsUAVRegister) { auto rs = DM.GetSerializedRootSignature(); if (!rs.empty()) { std::vector asVector = AddUAVParamterToRootSignature( - rs.data(), static_cast(rs.size()), toolsUAVRegister); + rs.data(), static_cast(rs.size()), ToolsUAVRegister); if (!asVector.empty()) { DM.ResetSerializedRootSignature(asVector); } @@ -268,13 +268,13 @@ static void AddUAVToShaderAttributeRootSignature(DxilModule &DM, } static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, - uint32_t toolsUAVRegister) { + uint32_t ToolsUAVRegister) { struct ReplacementRootSignature { std::string Name; std::vector Data; }; - std::vector replacementRootSignatures; + std::vector ReplacementRootSignatures; auto *subObjects = DM.GetSubobjects(); if (subObjects != nullptr) { for (auto const &subObject : subObjects->GetSubobjects()) { @@ -285,24 +285,23 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, constexpr bool notALocalRS = false; if (subObject.second->GetRootSignature(notALocalRS, Data, Size, nullptr)) { - std::vector extended = - AddUAVParamterToRootSignature(Data, Size, toolsUAVRegister); - if (!extended.empty()) { - replacementRootSignatures.push_back( - {subObject.first.str(), std::move(extended)}); + std::vector ExtendedRootSignature = + AddUAVParamterToRootSignature(Data, Size, ToolsUAVRegister); + if (!ExtendedRootSignature.empty()) { + ReplacementRootSignatures.push_back( + {subObject.first.str(), std::move(ExtendedRootSignature)}); } } } } - constexpr bool notALocalRS = false; - for (const ReplacementRootSignature &replacementRootSignature : - replacementRootSignatures) { - subObjects->RemoveSubobject(replacementRootSignature.Name); + constexpr bool NotALocalRootSignature = false; + for (const ReplacementRootSignature &Replacement : + ReplacementRootSignatures) { + subObjects->RemoveSubobject(Replacement.Name); subObjects->CreateRootSignature( - replacementRootSignature.Name, notALocalRS, - replacementRootSignature.Data.data(), - static_cast(replacementRootSignature.Data.size())); + Replacement.Name, NotALocalRootSignature, Replacement.Data.data(), + static_cast(Replacement.Data.size())); } } } @@ -313,10 +312,10 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, const char *name) { LLVMContext &Ctx = DM.GetModule()->getContext(); - for (const std::unique_ptr &existingUAV : DM.GetUAVs()) { - if (existingUAV->GetSpaceID() == toolsRegisterSpace && - existingUAV->GetLowerBound() == hlslBindIndex) { - return existingUAV.get(); + for (const std::unique_ptr &ExistingUAV : DM.GetUAVs()) { + if (ExistingUAV->GetSpaceID() == toolsRegisterSpace && + ExistingUAV->GetLowerBound() == hlslBindIndex) { + return ExistingUAV.get(); } } @@ -386,7 +385,7 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, return ret; } -void EraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction) { +void eraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction) { if (OpFunction != nullptr && OpFunction->user_empty()) { DM.GetOP()->RemoveFunction(OpFunction); OpFunction->eraseFromParent(); @@ -585,11 +584,11 @@ void ForEachDynamicallyIndexedResource( llvm::Function *CreateHandleFromBindingFn; llvm::Function *CreateHandleFromHeapFn; ~UnusedDeclarationCleanup() { - EraseIfUnused(DM, CreateHandleFn); - EraseIfUnused(DM, CreateHandleFromBindingFn); - EraseIfUnused(DM, CreateHandleFromHeapFn); + eraseIfUnused(DM, CreateHandleFn); + eraseIfUnused(DM, CreateHandleFromBindingFn); + eraseIfUnused(DM, CreateHandleFromHeapFn); } - } cleanup{DM, CreateHandleFn, CreateHandleFromBindingFn, + } Cleanup{DM, CreateHandleFn, CreateHandleFromBindingFn, CreateHandleFromHeapFn}; for (auto FI = CreateHandleFn->user_begin(); diff --git a/lib/DxilPIXPasses/PixPassHelpers.h b/lib/DxilPIXPasses/PixPassHelpers.h index 3d6e24d22f..5efdc4940c 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.h +++ b/lib/DxilPIXPasses/PixPassHelpers.h @@ -48,7 +48,7 @@ llvm::CallInst *CreateHandleForResource(hlsl::DxilModule &DM, hlsl::DxilResourceBase *resource, const char *name); llvm::Function *GetEntryFunction(hlsl::DxilModule &DM); -void EraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction); +void eraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction); std::vector GetAllInstrumentableFunctions(hlsl::DxilModule &DM); hlsl::DXIL::ShaderKind GetFunctionShaderKind(hlsl::DxilModule &DM, diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 443f734f5d..f4ce5bf855 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -578,12 +578,12 @@ class PixTest : public ::testing::Test { } void ValidateAccessTrackingMods(const char *hlsl, bool modsExpected); - void LoadSubobjectsFromContainerIntoModule(IDxcBlob *container, + void loadSubobjectsFromContainerIntoModule(IDxcBlob *Container, DxilModule &DM); - void VerifyGlobalRootSignaturesHaveToolsUAVs( - DxilSubobjects *subObjects, - const std::vector &expectedRootSignatureNames, - const std::vector &expectedShaderRegisters); + void verifyGlobalRootSignaturesHaveToolsUAVs( + DxilSubobjects *Subobjects, + const std::vector &ExpectedRootSignatureNames, + const std::vector &ExpectedShaderRegisters); class ModuleAndHangersOn { std::unique_ptr llvmContext; @@ -689,50 +689,52 @@ bool PixTest::InitSupport() { return true; } -static unsigned CountToolsUAVs(DxilModule &DM) { - unsigned count = 0; - for (const std::unique_ptr &uav : DM.GetUAVs()) { - if (uav->GetSpaceID() == static_cast(-2)) { - count++; +static unsigned countToolsUAVs(DxilModule &DM) { + unsigned Count = 0; + for (const std::unique_ptr &UAV : DM.GetUAVs()) { + if (UAV->GetSpaceID() == static_cast(-2)) { + Count++; } } - return count; + return Count; } -static int CountToolsUAVRecords(std::vector const &lines) { - int count = 0; - for (const std::string &line : lines) { - if (!line.empty() && line[0] == '!' && - line.find(", i32 -2, i32 ") != std::string::npos) { - count++; +static int countToolsUAVRecords(std::vector const &Lines) { + int Count = 0; + for (const std::string &Line : Lines) { + if (!Line.empty() && Line[0] == '!' && + Line.find(", i32 -2, i32 ") != std::string::npos) { + Count++; } } - return count; + return Count; } static bool -RootSignatureHasToolsUAV(const DxilVersionedRootSignatureDesc *rootSignature, - uint32_t shaderRegister) { - switch (rootSignature->Version) { +rootSignatureHasToolsUAV(const DxilVersionedRootSignatureDesc *RootSignature, + uint32_t ShaderRegister) { + switch (RootSignature->Version) { case DxilRootSignatureVersion::Version_1_0: { - const DxilRootSignatureDesc &desc = rootSignature->Desc_1_0; - for (uint32_t i = 0; i < desc.NumParameters; ++i) { - const DxilRootParameter ¶m = desc.pParameters[i]; - if (param.ParameterType == DxilRootParameterType::UAV && - param.Descriptor.RegisterSpace == static_cast(-2) && - param.Descriptor.ShaderRegister == shaderRegister) { + const DxilRootSignatureDesc &Desc = RootSignature->Desc_1_0; + for (uint32_t ParameterIndex = 0; ParameterIndex < Desc.NumParameters; + ++ParameterIndex) { + const DxilRootParameter &Parameter = Desc.pParameters[ParameterIndex]; + if (Parameter.ParameterType == DxilRootParameterType::UAV && + Parameter.Descriptor.RegisterSpace == static_cast(-2) && + Parameter.Descriptor.ShaderRegister == ShaderRegister) { return true; } } break; } case DxilRootSignatureVersion::Version_1_1: { - const DxilRootSignatureDesc1 &desc = rootSignature->Desc_1_1; - for (uint32_t i = 0; i < desc.NumParameters; ++i) { - const DxilRootParameter1 ¶m = desc.pParameters[i]; - if (param.ParameterType == DxilRootParameterType::UAV && - param.Descriptor.RegisterSpace == static_cast(-2) && - param.Descriptor.ShaderRegister == shaderRegister) { + const DxilRootSignatureDesc1 &Desc = RootSignature->Desc_1_1; + for (uint32_t ParameterIndex = 0; ParameterIndex < Desc.NumParameters; + ++ParameterIndex) { + const DxilRootParameter1 &Parameter = Desc.pParameters[ParameterIndex]; + if (Parameter.ParameterType == DxilRootParameterType::UAV && + Parameter.Descriptor.RegisterSpace == static_cast(-2) && + Parameter.Descriptor.ShaderRegister == ShaderRegister) { return true; } } @@ -742,67 +744,67 @@ RootSignatureHasToolsUAV(const DxilVersionedRootSignatureDesc *rootSignature, return false; } -void PixTest::LoadSubobjectsFromContainerIntoModule(IDxcBlob *container, +void PixTest::loadSubobjectsFromContainerIntoModule(IDxcBlob *Container, DxilModule &DM) { - const char *blobContent = - reinterpret_cast(container->GetBufferPointer()); - const unsigned blobSize = container->GetBufferSize(); - const hlsl::DxilContainerHeader *containerHeader = - hlsl::IsDxilContainerLike(blobContent, blobSize); - VERIFY_ARE_NOT_EQUAL(containerHeader, nullptr); - - const hlsl::DxilPartHeader *partHeader = - GetDxilPartByType(containerHeader, hlsl::DFCC_RuntimeData); - VERIFY_ARE_NOT_EQUAL(partHeader, nullptr); - - hlsl::RDAT::DxilRuntimeData rdat(GetDxilPartData(partHeader), - partHeader->PartSize); - std::unique_ptr subObjects(new DxilSubobjects()); - VERIFY_IS_TRUE(LoadSubobjectsFromRDAT(*subObjects, rdat)); - DM.ResetSubobjects(subObjects.release()); -} - -void PixTest::VerifyGlobalRootSignaturesHaveToolsUAVs( - DxilSubobjects *subObjects, - const std::vector &expectedRootSignatureNames, - const std::vector &expectedShaderRegisters) { - VERIFY_IS_NOT_NULL(subObjects); - - std::map foundRootSignatures; - for (const std::string &rootSignatureName : expectedRootSignatureNames) { - foundRootSignatures[rootSignatureName] = false; + const char *BlobContent = + reinterpret_cast(Container->GetBufferPointer()); + const unsigned BlobSize = Container->GetBufferSize(); + const hlsl::DxilContainerHeader *ContainerHeader = + hlsl::IsDxilContainerLike(BlobContent, BlobSize); + VERIFY_ARE_NOT_EQUAL(ContainerHeader, nullptr); + + const hlsl::DxilPartHeader *PartHeader = + GetDxilPartByType(ContainerHeader, hlsl::DFCC_RuntimeData); + VERIFY_ARE_NOT_EQUAL(PartHeader, nullptr); + + hlsl::RDAT::DxilRuntimeData RuntimeData(GetDxilPartData(PartHeader), + PartHeader->PartSize); + std::unique_ptr Subobjects(new DxilSubobjects()); + VERIFY_IS_TRUE(LoadSubobjectsFromRDAT(*Subobjects, RuntimeData)); + DM.ResetSubobjects(Subobjects.release()); +} + +void PixTest::verifyGlobalRootSignaturesHaveToolsUAVs( + DxilSubobjects *Subobjects, + const std::vector &ExpectedRootSignatureNames, + const std::vector &ExpectedShaderRegisters) { + VERIFY_IS_NOT_NULL(Subobjects); + + std::map FoundRootSignatures; + for (const std::string &RootSignatureName : ExpectedRootSignatureNames) { + FoundRootSignatures[RootSignatureName] = false; } - for (auto const &subObject : subObjects->GetSubobjects()) { - if (subObject.second->GetKind() != + for (auto const &Subobject : Subobjects->GetSubobjects()) { + if (Subobject.second->GetKind() != hlsl::DXIL::SubobjectKind::GlobalRootSignature) { continue; } - const std::string subObjectName = subObject.first.str(); - if (foundRootSignatures.find(subObjectName) == foundRootSignatures.end()) { + const std::string SubobjectName = Subobject.first.str(); + if (FoundRootSignatures.find(SubobjectName) == FoundRootSignatures.end()) { continue; } - const void *data = nullptr; - uint32_t size = 0; - constexpr bool notALocalRS = false; - VERIFY_IS_TRUE( - subObject.second->GetRootSignature(notALocalRS, data, size, nullptr)); + const void *Data = nullptr; + uint32_t Size = 0; + constexpr bool NotALocalRootSignature = false; + VERIFY_IS_TRUE(Subobject.second->GetRootSignature(NotALocalRootSignature, + Data, Size, nullptr)); - DxilVersionedRootSignatureDesc const *rootSignature = nullptr; - DeserializeRootSignature(data, size, &rootSignature); - for (uint32_t expectedShaderRegister : expectedShaderRegisters) { + DxilVersionedRootSignatureDesc const *RootSignature = nullptr; + DeserializeRootSignature(Data, Size, &RootSignature); + for (uint32_t ExpectedShaderRegister : ExpectedShaderRegisters) { VERIFY_IS_TRUE( - RootSignatureHasToolsUAV(rootSignature, expectedShaderRegister)); + rootSignatureHasToolsUAV(RootSignature, ExpectedShaderRegister)); } - DeleteRootSignature(rootSignature); - foundRootSignatures[subObjectName] = true; + DeleteRootSignature(RootSignature); + FoundRootSignatures[SubobjectName] = true; } - for (const std::map::value_type &foundRootSignature : - foundRootSignatures) { - VERIFY_IS_TRUE(foundRootSignature.second); + for (const std::map::value_type &FoundRootSignature : + FoundRootSignatures) { + VERIFY_IS_TRUE(FoundRootSignature.second); } } @@ -1039,10 +1041,10 @@ PassOutput PixTest::RunDxilNonUniformResourceIndexInstrumentation( outputText = BlobToUtf8(pText); - PassOutput result; - result.blob = pOptimizedModule; - result.lines = Tokenize(Disassemble(pOptimizedModule), "\n"); - return result; + PassOutput Result; + Result.blob = pOptimizedModule; + Result.lines = Tokenize(Disassemble(pOptimizedModule), "\n"); + return Result; } CComPtr @@ -1064,12 +1066,12 @@ PixTest::RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob) { return pOptimizedModule; } -static bool HasDeclaration(const std::string &disassembly, - const std::string &functionName); -static std::string FindDeclarationLine(const std::string &disassembly, - const std::string &functionName); -static bool HasDeclarationLine(const std::string &disassembly, - const std::string &declaration); +static bool hasDeclaration(const std::string &Disassembly, + const std::string &FunctionName); +static std::string findDeclarationLine(const std::string &Disassembly, + const std::string &FunctionName); +static bool hasDeclarationLine(const std::string &Disassembly, + const std::string &Declaration); TEST_F(PixTest, AddToASPayload) { @@ -1112,30 +1114,30 @@ void MSMain( )"; auto as = Compile(m_dllSupport, hlsl, L"as_6_6", {}, L"ASMain"); - const std::string originalDispatchMeshDeclaration = - FindDeclarationLine(Disassemble(as), "dx.op.dispatchMesh"); - VERIFY_IS_FALSE(originalDispatchMeshDeclaration.empty()); + const std::string OriginalDispatchMeshDeclaration = + findDeclarationLine(Disassemble(as), "dx.op.dispatchMesh"); + VERIFY_IS_FALSE(OriginalDispatchMeshDeclaration.empty()); - CComPtr asOutput = + CComPtr ASOutput = RunDxilPIXAddTidToAmplificationShaderPayloadPass(as); - VERIFY_IS_FALSE(HasDeclarationLine(Disassemble(asOutput), - originalDispatchMeshDeclaration)); + VERIFY_IS_FALSE(hasDeclarationLine(Disassemble(ASOutput), + OriginalDispatchMeshDeclaration)); auto ms = Compile(m_dllSupport, hlsl, L"ms_6_6", {}, L"MSMain"); - const std::string originalGetMeshPayloadDeclaration = - FindDeclarationLine(Disassemble(ms), "dx.op.getMeshPayload"); - VERIFY_IS_FALSE(originalGetMeshPayloadDeclaration.empty()); + const std::string OriginalGetMeshPayloadDeclaration = + findDeclarationLine(Disassemble(ms), "dx.op.getMeshPayload"); + VERIFY_IS_FALSE(OriginalGetMeshPayloadDeclaration.empty()); - CComPtr msOutput = RunDxilPIXMeshShaderOutputPass(ms); - const std::string meshDisassembly = Disassemble(msOutput); + CComPtr MSOutput = RunDxilPIXMeshShaderOutputPass(ms); + const std::string MeshDisassembly = Disassemble(MSOutput); VERIFY_IS_FALSE( - HasDeclarationLine(meshDisassembly, originalGetMeshPayloadDeclaration)); + hasDeclarationLine(MeshDisassembly, OriginalGetMeshPayloadDeclaration)); VERIFY_IS_FALSE( - HasDeclaration(meshDisassembly, "dx.op.storeVertexOutput.i32")); + hasDeclaration(MeshDisassembly, "dx.op.storeVertexOutput.i32")); VERIFY_IS_FALSE( - HasDeclaration(meshDisassembly, "dx.op.storeVertexOutput.i16")); + hasDeclaration(MeshDisassembly, "dx.op.storeVertexOutput.i16")); VERIFY_IS_FALSE( - HasDeclaration(meshDisassembly, "dx.op.storeVertexOutput.f16")); + hasDeclaration(MeshDisassembly, "dx.op.storeVertexOutput.f16")); } unsigned FindOrAddVSInSignatureElementForInstanceOrVertexID( hlsl::DxilSignature &InputSignature, hlsl::DXIL::SemanticKind semanticKind); @@ -3214,7 +3216,7 @@ float4 main(int i : A, float j : B) : SV_TARGET } TEST_F(PixTest, ToolsUav_TwoPixPassesShareOneResource) { - const char *source = R"x( + const char *Source = R"x( RWByteAddressBuffer output : register(u0); [numthreads(1, 1, 1)] @@ -3223,20 +3225,20 @@ void main(uint3 tid : SV_DispatchThreadID) output.Store(4 * tid.x, tid.x); })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); - PassOutput debugOutput = RunDebugPass(compiled); - PassOutput accessOutput = RunShaderAccessTrackingPass(debugOutput.blob); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"cs_6_2", {L"-Od"}); + PassOutput DebugOutput = RunDebugPass(Compiled); + PassOutput AccessOutput = RunShaderAccessTrackingPass(DebugOutput.blob); - ModuleAndHangersOn moduleEtc(accessOutput.blob); - VERIFY_ARE_EQUAL(1u, CountToolsUAVs(moduleEtc.GetDxilModule())); - VerifyInstrumentedModuleIsValid( - accessOutput.blob, + ModuleAndHangersOn ModuleEtc(AccessOutput.blob); + VERIFY_ARE_EQUAL(1u, countToolsUAVs(ModuleEtc.GetDxilModule())); + verifyInstrumentedModuleIsValid( + AccessOutput.blob, "debug instrumentation followed by shader access tracking"); } TEST_F(PixTest, ToolsUav_LibraryWithTwoEntryPointsCreatesOnePair) { - const char *source = R"x( + const char *Source = R"x( struct [raypayload] MyPayload { float2 barycentrics : read(caller) : write(caller,anyhit); @@ -3256,15 +3258,15 @@ void MissTwo(inout MyPayload payload) } )x"; - CComPtr compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); - CComPtr output = RunDxilPIXDXRInvocationsLog(compiled); + CComPtr Compiled = Compile(m_dllSupport, Source, L"lib_6_6", {}); + CComPtr Output = RunDxilPIXDXRInvocationsLog(Compiled); - std::vector lines = Tokenize(Disassemble(output), "\n"); - VERIFY_ARE_EQUAL(2, CountToolsUAVRecords(lines)); + std::vector Lines = Tokenize(Disassemble(Output), "\n"); + VERIFY_ARE_EQUAL(2, countToolsUAVRecords(Lines)); } TEST_F(PixTest, ToolsUav_ExtendsEveryGlobalRootSignatureSubobject) { - const char *source = R"x( + const char *Source = R"x( GlobalRootSignature firstRootSignature = {"CBV(b0)"}; GlobalRootSignature secondRootSignature = {"SRV(t0)"}; @@ -3302,249 +3304,249 @@ void MyMiss(inout MyPayload payload) } )x"; - CComPtr compiled = Compile(m_dllSupport, source, L"lib_6_6", {}); - ModuleAndHangersOn moduleEtc(compiled); - DxilModule &DM = moduleEtc.GetDxilModule(); - LoadSubobjectsFromContainerIntoModule(compiled, DM); + CComPtr Compiled = Compile(m_dllSupport, Source, L"lib_6_6", {}); + ModuleAndHangersOn ModuleEtc(Compiled); + DxilModule &DM = ModuleEtc.GetDxilModule(); + loadSubobjectsFromContainerIntoModule(Compiled, DM); PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_CountUAV_Handle"); PIXPassHelpers::CreateGlobalUAVResource(DM, 1, "PIX_LogUAV_Handle"); - VerifyGlobalRootSignaturesHaveToolsUAVs( + verifyGlobalRootSignaturesHaveToolsUAVs( DM.GetSubobjects(), {"firstRootSignature", "secondRootSignature"}, {0, 1}); } TEST_F(PixTest, DebugInstrumentation_RawBufferShaderFlagDeclared) { - const char *source = R"x( + const char *Source = R"x( [numthreads(1, 1, 1)] void main(uint threadId : SV_DispatchThreadID) { })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"cs_6_2", {L"-Od"}); - PassOutput output = RunDebugPass(compiled); - std::vector lines = Tokenize(Disassemble(output.blob), "\n"); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"cs_6_2", {L"-Od"}); + PassOutput Output = RunDebugPass(Compiled); + std::vector Lines = Tokenize(Disassemble(Output.blob), "\n"); constexpr uint64_t EnableRawAndStructuredBuffers = 0x10; - bool foundShaderFlags = false; - uint64_t shaderFlags = 0; - const std::string tagPrefix = "!{i32 0, i64 "; - for (const std::string &line : lines) { - const std::string::size_type tagStart = line.find(tagPrefix); - if (tagStart == std::string::npos) { + bool FoundShaderFlags = false; + uint64_t ShaderFlags = 0; + const std::string TagPrefix = "!{i32 0, i64 "; + for (const std::string &Line : Lines) { + const std::string::size_type TagStart = Line.find(TagPrefix); + if (TagStart == std::string::npos) { continue; } - shaderFlags = - strtoull(line.c_str() + tagStart + tagPrefix.length(), nullptr, 10); - foundShaderFlags = true; + ShaderFlags = + strtoull(Line.c_str() + TagStart + TagPrefix.length(), nullptr, 10); + FoundShaderFlags = true; break; } - VERIFY_IS_TRUE(foundShaderFlags); + VERIFY_IS_TRUE(FoundShaderFlags); VERIFY_ARE_EQUAL(EnableRawAndStructuredBuffers, - shaderFlags & EnableRawAndStructuredBuffers); - VerifyInstrumentedModuleIsValid(output.blob, + ShaderFlags & EnableRawAndStructuredBuffers); + verifyInstrumentedModuleIsValid(Output.blob, "debug instrumentation shader flags"); } TEST_F(PixTest, ToolsUav_RootSignatureSerializationFailurePreservesSignature) { - const char *source = R"x( + const char *Source = R"x( [numthreads(1, 1, 1)] void main() { })x"; - DxilDescriptorRange range = {}; - range.RangeType = DxilDescriptorRangeType::UAV; - range.NumDescriptors = 1; - range.BaseShaderRegister = 0; - range.RegisterSpace = static_cast(-2); - range.OffsetInDescriptorsFromTableStart = DxilDescriptorRangeOffsetAppend; - - DxilRootParameter parameter = {}; - parameter.ParameterType = DxilRootParameterType::DescriptorTable; - parameter.DescriptorTable.NumDescriptorRanges = 1; - parameter.DescriptorTable.pDescriptorRanges = ⦥ - parameter.ShaderVisibility = DxilShaderVisibility::All; - - DxilVersionedRootSignatureDesc rootSignature = {}; - rootSignature.Version = DxilRootSignatureVersion::Version_1_0; - rootSignature.Desc_1_0.NumParameters = 1; - rootSignature.Desc_1_0.pParameters = ¶meter; - rootSignature.Desc_1_0.Flags = DxilRootSignatureFlags::None; - - CComPtr serializedRootSignature; - CComPtr errorBlob; - SerializeRootSignature(&rootSignature, &serializedRootSignature, &errorBlob, + DxilDescriptorRange Range = {}; + Range.RangeType = DxilDescriptorRangeType::UAV; + Range.NumDescriptors = 1; + Range.BaseShaderRegister = 0; + Range.RegisterSpace = static_cast(-2); + Range.OffsetInDescriptorsFromTableStart = DxilDescriptorRangeOffsetAppend; + + DxilRootParameter Parameter = {}; + Parameter.ParameterType = DxilRootParameterType::DescriptorTable; + Parameter.DescriptorTable.NumDescriptorRanges = 1; + Parameter.DescriptorTable.pDescriptorRanges = &Range; + Parameter.ShaderVisibility = DxilShaderVisibility::All; + + DxilVersionedRootSignatureDesc RootSignature = {}; + RootSignature.Version = DxilRootSignatureVersion::Version_1_0; + RootSignature.Desc_1_0.NumParameters = 1; + RootSignature.Desc_1_0.pParameters = &Parameter; + RootSignature.Desc_1_0.Flags = DxilRootSignatureFlags::None; + + CComPtr SerializedRootSignature; + CComPtr ErrorBlob; + SerializeRootSignature(&RootSignature, &SerializedRootSignature, &ErrorBlob, true); - VERIFY_IS_NOT_NULL(serializedRootSignature); - - const uint8_t *serializedData = - static_cast(serializedRootSignature->GetBufferPointer()); - std::vector originalRootSignature( - serializedData, - serializedData + serializedRootSignature->GetBufferSize()); - - CComPtr compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); - ModuleAndHangersOn moduleEtc(compiled); - DxilModule &DM = moduleEtc.GetDxilModule(); - DM.ResetSerializedRootSignature(originalRootSignature); - - std::unique_ptr subObjects(new DxilSubobjects()); - constexpr bool notALocalRootSignature = false; - subObjects->CreateRootSignature( - "testRootSignature", notALocalRootSignature, originalRootSignature.data(), - static_cast(originalRootSignature.size())); - DM.ResetSubobjects(subObjects.release()); + VERIFY_IS_NOT_NULL(SerializedRootSignature); + + const uint8_t *SerializedData = + static_cast(SerializedRootSignature->GetBufferPointer()); + std::vector OriginalRootSignature( + SerializedData, + SerializedData + SerializedRootSignature->GetBufferSize()); + + CComPtr Compiled = Compile(m_dllSupport, Source, L"cs_6_0", {}); + ModuleAndHangersOn ModuleEtc(Compiled); + DxilModule &DM = ModuleEtc.GetDxilModule(); + DM.ResetSerializedRootSignature(OriginalRootSignature); + + std::unique_ptr Subobjects(new DxilSubobjects()); + constexpr bool NotALocalRootSignature = false; + Subobjects->CreateRootSignature( + "testRootSignature", NotALocalRootSignature, OriginalRootSignature.data(), + static_cast(OriginalRootSignature.size())); + DM.ResetSubobjects(Subobjects.release()); PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_TestUAV"); - const std::vector &actualRootSignature = + const std::vector &ActualRootSignature = DM.GetSerializedRootSignature(); - VERIFY_ARE_EQUAL(originalRootSignature.size(), actualRootSignature.size()); - VERIFY_IS_TRUE(std::equal(originalRootSignature.begin(), - originalRootSignature.end(), - actualRootSignature.begin())); - - bool foundRootSignature = false; - for (auto const &subObject : DM.GetSubobjects()->GetSubobjects()) { - if (subObject.first != "testRootSignature") { + VERIFY_ARE_EQUAL(OriginalRootSignature.size(), ActualRootSignature.size()); + VERIFY_IS_TRUE(std::equal(OriginalRootSignature.begin(), + OriginalRootSignature.end(), + ActualRootSignature.begin())); + + bool FoundRootSignature = false; + for (auto const &Subobject : DM.GetSubobjects()->GetSubobjects()) { + if (Subobject.first != "testRootSignature") { continue; } - const void *data = nullptr; - uint32_t size = 0; - VERIFY_IS_TRUE(subObject.second->GetRootSignature(notALocalRootSignature, - data, size, nullptr)); - VERIFY_ARE_EQUAL(originalRootSignature.size(), static_cast(size)); - VERIFY_IS_TRUE(std::equal(originalRootSignature.begin(), - originalRootSignature.end(), - static_cast(data))); - foundRootSignature = true; + const void *Data = nullptr; + uint32_t Size = 0; + VERIFY_IS_TRUE(Subobject.second->GetRootSignature(NotALocalRootSignature, + Data, Size, nullptr)); + VERIFY_ARE_EQUAL(OriginalRootSignature.size(), static_cast(Size)); + VERIFY_IS_TRUE(std::equal(OriginalRootSignature.begin(), + OriginalRootSignature.end(), + static_cast(Data))); + FoundRootSignature = true; } - VERIFY_IS_TRUE(foundRootSignature); + VERIFY_IS_TRUE(FoundRootSignature); } TEST_F(PixTest, ToolsUav_ExtendingRootSignaturePreservesUnrelatedParameterFlags) { - const char *source = R"x( + const char *Source = R"x( [numthreads(1, 1, 1)] void main() { })x"; - DxilRootParameter1 parameters[2] = {}; - parameters[0].ParameterType = DxilRootParameterType::UAV; - parameters[0].Descriptor.RegisterSpace = static_cast(-2); - parameters[0].Descriptor.ShaderRegister = 0; - parameters[0].Descriptor.Flags = DxilRootDescriptorFlags::None; - parameters[0].ShaderVisibility = DxilShaderVisibility::All; - - parameters[1].ParameterType = DxilRootParameterType::CBV; - parameters[1].Descriptor.RegisterSpace = 0; - parameters[1].Descriptor.ShaderRegister = 0; - parameters[1].Descriptor.Flags = DxilRootDescriptorFlags::DataVolatile; - parameters[1].ShaderVisibility = DxilShaderVisibility::All; - - DxilVersionedRootSignatureDesc rootSignature = {}; - rootSignature.Version = DxilRootSignatureVersion::Version_1_1; - rootSignature.Desc_1_1.NumParameters = 2; - rootSignature.Desc_1_1.pParameters = parameters; - rootSignature.Desc_1_1.Flags = DxilRootSignatureFlags::None; - - CComPtr serializedRootSignature; - CComPtr errorBlob; - SerializeRootSignature(&rootSignature, &serializedRootSignature, &errorBlob, + DxilRootParameter1 Parameters[2] = {}; + Parameters[0].ParameterType = DxilRootParameterType::UAV; + Parameters[0].Descriptor.RegisterSpace = static_cast(-2); + Parameters[0].Descriptor.ShaderRegister = 0; + Parameters[0].Descriptor.Flags = DxilRootDescriptorFlags::None; + Parameters[0].ShaderVisibility = DxilShaderVisibility::All; + + Parameters[1].ParameterType = DxilRootParameterType::CBV; + Parameters[1].Descriptor.RegisterSpace = 0; + Parameters[1].Descriptor.ShaderRegister = 0; + Parameters[1].Descriptor.Flags = DxilRootDescriptorFlags::DataVolatile; + Parameters[1].ShaderVisibility = DxilShaderVisibility::All; + + DxilVersionedRootSignatureDesc RootSignature = {}; + RootSignature.Version = DxilRootSignatureVersion::Version_1_1; + RootSignature.Desc_1_1.NumParameters = 2; + RootSignature.Desc_1_1.pParameters = Parameters; + RootSignature.Desc_1_1.Flags = DxilRootSignatureFlags::None; + + CComPtr SerializedRootSignature; + CComPtr ErrorBlob; + SerializeRootSignature(&RootSignature, &SerializedRootSignature, &ErrorBlob, true); - VERIFY_IS_NOT_NULL(serializedRootSignature); + VERIFY_IS_NOT_NULL(SerializedRootSignature); - const uint8_t *serializedData = - static_cast(serializedRootSignature->GetBufferPointer()); - std::vector originalRootSignature( - serializedData, - serializedData + serializedRootSignature->GetBufferSize()); + const uint8_t *SerializedData = + static_cast(SerializedRootSignature->GetBufferPointer()); + std::vector OriginalRootSignature( + SerializedData, + SerializedData + SerializedRootSignature->GetBufferSize()); - CComPtr compiled = Compile(m_dllSupport, source, L"cs_6_0", {}); - ModuleAndHangersOn moduleEtc(compiled); - DxilModule &DM = moduleEtc.GetDxilModule(); - DM.ResetSerializedRootSignature(originalRootSignature); + CComPtr Compiled = Compile(m_dllSupport, Source, L"cs_6_0", {}); + ModuleAndHangersOn ModuleEtc(Compiled); + DxilModule &DM = ModuleEtc.GetDxilModule(); + DM.ResetSerializedRootSignature(OriginalRootSignature); PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_TestUAV0"); { - const std::vector &bytes = DM.GetSerializedRootSignature(); - DxilVersionedRootSignatureDesc const *afterNoOp = nullptr; - DeserializeRootSignature(bytes.data(), static_cast(bytes.size()), - &afterNoOp); - VERIFY_ARE_EQUAL(afterNoOp->Desc_1_1.NumParameters, 2u); - VERIFY_IS_TRUE(afterNoOp->Desc_1_1.pParameters[1].Descriptor.Flags == + const std::vector &Bytes = DM.GetSerializedRootSignature(); + DxilVersionedRootSignatureDesc const *AfterNoOp = nullptr; + DeserializeRootSignature(Bytes.data(), static_cast(Bytes.size()), + &AfterNoOp); + VERIFY_ARE_EQUAL(AfterNoOp->Desc_1_1.NumParameters, 2u); + VERIFY_IS_TRUE(AfterNoOp->Desc_1_1.pParameters[1].Descriptor.Flags == DxilRootDescriptorFlags::DataVolatile); - DeleteRootSignature(afterNoOp); + DeleteRootSignature(AfterNoOp); } PIXPassHelpers::CreateGlobalUAVResource(DM, 1, "PIX_TestUAV1"); { - const std::vector &bytes = DM.GetSerializedRootSignature(); - DxilVersionedRootSignatureDesc const *afterAdd = nullptr; - DeserializeRootSignature(bytes.data(), static_cast(bytes.size()), - &afterAdd); - VERIFY_ARE_EQUAL(afterAdd->Desc_1_1.NumParameters, 3u); - VERIFY_IS_TRUE(afterAdd->Desc_1_1.pParameters[1].Descriptor.Flags == + const std::vector &Bytes = DM.GetSerializedRootSignature(); + DxilVersionedRootSignatureDesc const *AfterAdd = nullptr; + DeserializeRootSignature(Bytes.data(), static_cast(Bytes.size()), + &AfterAdd); + VERIFY_ARE_EQUAL(AfterAdd->Desc_1_1.NumParameters, 3u); + VERIFY_IS_TRUE(AfterAdd->Desc_1_1.pParameters[1].Descriptor.Flags == DxilRootDescriptorFlags::DataVolatile); - VERIFY_ARE_EQUAL(afterAdd->Desc_1_1.pParameters[2].Descriptor.RegisterSpace, + VERIFY_ARE_EQUAL(AfterAdd->Desc_1_1.pParameters[2].Descriptor.RegisterSpace, static_cast(-2)); VERIFY_ARE_EQUAL( - afterAdd->Desc_1_1.pParameters[2].Descriptor.ShaderRegister, 1u); - VERIFY_IS_TRUE(afterAdd->Desc_1_1.pParameters[2].Descriptor.Flags == + AfterAdd->Desc_1_1.pParameters[2].Descriptor.ShaderRegister, 1u); + VERIFY_IS_TRUE(AfterAdd->Desc_1_1.pParameters[2].Descriptor.Flags == DxilRootDescriptorFlags::None); - DeleteRootSignature(afterAdd); + DeleteRootSignature(AfterAdd); } } -static bool HasUnusedDeclaration(std::vector const &lines, - std::string const &functionName) { - bool declared = false; - for (const std::string &line : lines) { - if (line.find("declare") != std::string::npos && - line.find(functionName) != std::string::npos) { - declared = true; +static bool hasUnusedDeclaration(std::vector const &Lines, + std::string const &FunctionName) { + bool Declared = false; + for (const std::string &Line : Lines) { + if (Line.find("declare") != std::string::npos && + Line.find(FunctionName) != std::string::npos) { + Declared = true; } - if (line.find("call") != std::string::npos && - line.find(functionName) != std::string::npos) { + if (Line.find("call") != std::string::npos && + Line.find(FunctionName) != std::string::npos) { return false; } } - return declared; + return Declared; } -static bool HasDeclaration(const std::string &disassembly, - const std::string &functionName) { - for (const std::string &line : Tokenize(disassembly, "\n")) { - if (line.find("declare") != std::string::npos && - line.find(functionName) != std::string::npos) { +static bool hasDeclaration(const std::string &Disassembly, + const std::string &FunctionName) { + for (const std::string &Line : Tokenize(Disassembly, "\n")) { + if (Line.find("declare") != std::string::npos && + Line.find(FunctionName) != std::string::npos) { return true; } } return false; } -static std::string FindDeclarationLine(const std::string &disassembly, - const std::string &functionName) { - for (const std::string &line : Tokenize(disassembly, "\n")) { - if (line.find("declare") != std::string::npos && - line.find(functionName) != std::string::npos) { - return line; +static std::string findDeclarationLine(const std::string &Disassembly, + const std::string &FunctionName) { + for (const std::string &Line : Tokenize(Disassembly, "\n")) { + if (Line.find("declare") != std::string::npos && + Line.find(FunctionName) != std::string::npos) { + return Line; } } return {}; } -static bool HasDeclarationLine(const std::string &disassembly, - const std::string &declaration) { - for (const std::string &line : Tokenize(disassembly, "\n")) { - if (line == declaration) { +static bool hasDeclarationLine(const std::string &Disassembly, + const std::string &Declaration) { + for (const std::string &Line : Tokenize(Disassembly, "\n")) { + if (Line == Declaration) { return true; } } @@ -3552,90 +3554,90 @@ static bool HasDeclarationLine(const std::string &disassembly, } TEST_F(PixTest, ConstantColor_UnusedIntOverloadIsErased) { - const char *source = R"x( + const char *Source = R"x( float4 main() : SV_Target { return float4(1, 2, 3, 4); })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - SinglePassOutput output = - RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + SinglePassOutput Output = + runSinglePass(Compiled, L"-hlsl-dxil-constantColor"); - VERIFY_IS_FALSE(HasUnusedDeclaration(output.Lines, "dx.op.storeOutput.i32")); - VerifyInstrumentedModuleIsValid(output.Module, + VERIFY_IS_FALSE(hasUnusedDeclaration(Output.Lines, "dx.op.storeOutput.i32")); + verifyInstrumentedModuleIsValid(Output.Module, "constant-colour substitution"); } TEST_F(PixTest, ConstantColor_NoTargetOverloadsAreErased) { - const char *source = R"x( + const char *Source = R"x( [numthreads(1, 1, 1)] void main() { })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"cs_6_0", {L"-Od"}); - SinglePassOutput output = - RunSinglePass(compiled, L"-hlsl-dxil-constantColor"); - const std::string disassembly = Disassemble(output.Module); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"cs_6_0", {L"-Od"}); + SinglePassOutput Output = + runSinglePass(Compiled, L"-hlsl-dxil-constantColor"); + const std::string Disassembly = Disassemble(Output.Module); - VerifyInstrumentedModuleIsValid( - output.Module, "constant-colour substitution with no target"); - VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.storeOutput.f32")); - VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.storeOutput.i32")); + verifyInstrumentedModuleIsValid( + Output.Module, "constant-colour substitution with no target"); + VERIFY_IS_FALSE(hasDeclaration(Disassembly, "dx.op.storeOutput.f32")); + VERIFY_IS_FALSE(hasDeclaration(Disassembly, "dx.op.storeOutput.i32")); } TEST_F(PixTest, RemoveDiscards_UnusedDiscardOverloadIsErased) { - const char *source = R"x( + const char *Source = R"x( float4 main() : SV_Target { return float4(1, 2, 3, 4); })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - SinglePassOutput output = - RunSinglePass(compiled, L"-hlsl-dxil-remove-discards"); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + SinglePassOutput Output = + runSinglePass(Compiled, L"-hlsl-dxil-remove-discards"); - VERIFY_IS_FALSE(HasUnusedDeclaration(output.Lines, "dx.op.discard")); - VerifyInstrumentedModuleIsValid(output.Module, + VERIFY_IS_FALSE(hasUnusedDeclaration(Output.Lines, "dx.op.discard")); + verifyInstrumentedModuleIsValid(Output.Module, "discard removal with no discard"); } TEST_F(PixTest, OperationCacheCleanup_RemovesErasedFunctions) { - const char *source = R"x( + const char *Source = R"x( float4 main() : SV_Target { return float4(1, 2, 3, 4); })x"; - CComPtr compiled = Compile(m_dllSupport, source, L"ps_6_0", {}); - ModuleAndHangersOn moduleEtc(compiled); - DxilModule &DM = moduleEtc.GetDxilModule(); + CComPtr Compiled = Compile(m_dllSupport, Source, L"ps_6_0", {}); + ModuleAndHangersOn ModuleEtc(Compiled); + DxilModule &DM = ModuleEtc.GetDxilModule(); OP *HlslOP = DM.GetOP(); - llvm::Function *discard = + llvm::Function *Discard = HlslOP->GetOpFunc(DXIL::OpCode::Discard, llvm::Type::getVoidTy(DM.GetModule()->getContext())); VERIFY_ARE_EQUAL(1u, static_cast( HlslOP->GetOpFuncList(DXIL::OpCode::Discard).size())); - PIXPassHelpers::EraseIfUnused(DM, discard); + PIXPassHelpers::eraseIfUnused(DM, Discard); VERIFY_ARE_EQUAL(0u, static_cast( HlslOP->GetOpFuncList(DXIL::OpCode::Discard).size())); - llvm::Function *recreated = + llvm::Function *Recreated = HlslOP->GetOpFunc(DXIL::OpCode::Discard, llvm::Type::getVoidTy(DM.GetModule()->getContext())); - VERIFY_IS_NOT_NULL(recreated); - PIXPassHelpers::EraseIfUnused(DM, recreated); + VERIFY_IS_NOT_NULL(Recreated); + PIXPassHelpers::eraseIfUnused(DM, Recreated); } TEST_F(PixTest, DynamicResourceCleanup_VisitorStopsEarly) { - const char *source = R"x( + const char *Source = R"x( Texture2D textures[] : register(t0); float4 main(float2 uv : TEXCOORD0) : SV_Target @@ -3643,18 +3645,18 @@ float4 main(float2 uv : TEXCOORD0) : SV_Target return textures[(uint)uv.x].Load(int3(0, 0, 0)); })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); - ModuleAndHangersOn moduleEtc(compiled); - DxilModule &DM = moduleEtc.GetDxilModule(); - bool visitorCalled = false; + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + ModuleAndHangersOn ModuleEtc(Compiled); + DxilModule &DM = ModuleEtc.GetDxilModule(); + bool VisitorCalled = false; PIXPassHelpers::ForEachDynamicallyIndexedResource( - DM, [&visitorCalled](bool, llvm::Instruction *, llvm::Value *) { - visitorCalled = true; + DM, [&VisitorCalled](bool, llvm::Instruction *, llvm::Value *) { + VisitorCalled = true; return false; }); - VERIFY_IS_TRUE(visitorCalled); + VERIFY_IS_TRUE(VisitorCalled); OP *HlslOP = DM.GetOP(); VERIFY_ARE_EQUAL( 0u, @@ -3759,9 +3761,9 @@ void PixTest::TestNuriCase(const char *source, const wchar_t *target, Compile(m_dllSupport, source, target, compilationOptions); std::string outputText; - PassOutput output = + PassOutput Output = RunDxilNonUniformResourceIndexInstrumentation(compiledLib, outputText); - const std::vector &dxilLines = output.lines; + const std::vector &dxilLines = Output.lines; VERIFY_ARE_EQUAL(NuriGetWaveInstructionCount(dxilLines), expectedResult); @@ -3812,7 +3814,7 @@ TEST_F(PixTest, NonUniformResourceIndex_QualifiedCleanupValidates) { return; } - const char *source = R"x( + const char *Source = R"x( Texture2D textures[] : register(t0); float4 main(float2 uv : TEXCOORD0) : SV_Target @@ -3821,18 +3823,18 @@ float4 main(float2 uv : TEXCOORD0) : SV_Target return textures[NonUniformResourceIndex(index)].Load(int3(0, 0, 0)); })x"; - CComPtr compiled = - Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); - std::string outputText; - PassOutput output = - RunDxilNonUniformResourceIndexInstrumentation(compiled, outputText); - const std::string disassembly = Disassemble(output.blob); + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_6", {L"-Od"}); + std::string OutputText; + PassOutput Output = + RunDxilNonUniformResourceIndexInstrumentation(Compiled, OutputText); + const std::string Disassembly = Disassemble(Output.blob); - VerifyInstrumentedModuleIsValid( - output.blob, "qualified non-uniform resource index instrumentation"); - VERIFY_ARE_EQUAL(0u, NuriGetWaveInstructionCount(output.lines)); - VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.waveActiveAllEqual.i32")); - VERIFY_IS_FALSE(HasDeclaration(disassembly, "dx.op.atomicBinOp.i32")); + verifyInstrumentedModuleIsValid( + Output.blob, "qualified non-uniform resource index instrumentation"); + VERIFY_ARE_EQUAL(0u, NuriGetWaveInstructionCount(Output.lines)); + VERIFY_IS_FALSE(hasDeclaration(Disassembly, "dx.op.waveActiveAllEqual.i32")); + VERIFY_IS_FALSE(hasDeclaration(Disassembly, "dx.op.atomicBinOp.i32")); } TEST_F(PixTest, NonUniformResourceIndex_DescriptorHeap) { @@ -4247,7 +4249,7 @@ void main() { foundDebugBreak = true; } VERIFY_IS_FALSE(foundDebugBreak); - VerifyInstrumentedModuleIsValid(output.blob, + verifyInstrumentedModuleIsValid(output.blob, "debug-break instrumentation with no call"); } From ad562073cd114bde22319bca376afea2b096742f Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 3 Sep 2026 11:27:25 -0700 Subject: [PATCH 21/39] [PIX] Name the root signature parameter Apply the explicitly approved naming exception to ExtendRootSig's existing parameter without changing any other pre-existing identifier. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/DxilPIXPasses/PixPassHelpers.cpp | 34 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index 2c572e4cf4..c16d8a2fa3 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -200,36 +200,36 @@ constexpr uint32_t toolsRegisterSpace = static_cast(-2); // Returns whether a parameter was appended. template -bool ExtendRootSig(RootSigDesc &rootSigDesc, uint32_t ToolsUAVRegister) { - auto *existingParams = rootSigDesc.pParameters; - for (uint32_t i = 0; i < rootSigDesc.NumParameters; ++i) { - if (rootSigDesc.pParameters[i].ParameterType == +bool ExtendRootSig(RootSigDesc &RootSignatureDesc, uint32_t ToolsUAVRegister) { + auto *existingParams = RootSignatureDesc.pParameters; + for (uint32_t i = 0; i < RootSignatureDesc.NumParameters; ++i) { + if (RootSignatureDesc.pParameters[i].ParameterType == DxilRootParameterType::UAV) { - if (rootSigDesc.pParameters[i].Descriptor.RegisterSpace == + if (RootSignatureDesc.pParameters[i].Descriptor.RegisterSpace == toolsRegisterSpace && - rootSigDesc.pParameters[i].Descriptor.ShaderRegister == + RootSignatureDesc.pParameters[i].Descriptor.ShaderRegister == ToolsUAVRegister) { // Already added return false; } } } - auto *newParams = new RootParameterDesc[rootSigDesc.NumParameters + 1]; + auto *newParams = new RootParameterDesc[RootSignatureDesc.NumParameters + 1]; if (existingParams != nullptr) { memcpy(newParams, existingParams, - rootSigDesc.NumParameters * sizeof(RootParameterDesc)); + RootSignatureDesc.NumParameters * sizeof(RootParameterDesc)); delete[] existingParams; } - rootSigDesc.pParameters = newParams; - rootSigDesc.pParameters[rootSigDesc.NumParameters].ParameterType = + RootSignatureDesc.pParameters = newParams; + RootSignatureDesc.pParameters[RootSignatureDesc.NumParameters].ParameterType = DxilRootParameterType::UAV; - rootSigDesc.pParameters[rootSigDesc.NumParameters].Descriptor.RegisterSpace = - toolsRegisterSpace; - rootSigDesc.pParameters[rootSigDesc.NumParameters].Descriptor.ShaderRegister = - ToolsUAVRegister; - rootSigDesc.pParameters[rootSigDesc.NumParameters].ShaderVisibility = - DxilShaderVisibility::All; - rootSigDesc.NumParameters++; + RootSignatureDesc.pParameters[RootSignatureDesc.NumParameters] + .Descriptor.RegisterSpace = toolsRegisterSpace; + RootSignatureDesc.pParameters[RootSignatureDesc.NumParameters] + .Descriptor.ShaderRegister = ToolsUAVRegister; + RootSignatureDesc.pParameters[RootSignatureDesc.NumParameters] + .ShaderVisibility = DxilShaderVisibility::All; + RootSignatureDesc.NumParameters++; return true; } From a1fd01162f92fb18f42841c5ab5862e5607c7108 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 3 Sep 2026 11:44:42 -0700 Subject: [PATCH 22/39] [PIX] Validate known metadata before stripping Reject known PIX metadata with an invalid attachment location or payload before removing it for DXIL validation. Add controls for function metadata, metadata on the wrong instruction type, and malformed metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819bcd16-83fc-4619-ac1a-b12ee50a2fe8 --- tools/clang/unittests/HLSL/PixTest.cpp | 172 +++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 10 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 440d961288..9d9131de5d 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -50,6 +50,7 @@ #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringSwitch.h" #include "llvm/Bitcode/ReaderWriter.h" +#include "llvm/IR/Constants.h" #include "llvm/IR/DebugInfo.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/IntrinsicInst.h" @@ -165,6 +166,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(Validation_ControlValidModulePasses) TEST_METHOD(Validation_ControlInvalidModuleFails) TEST_METHOD(Validation_ControlNonPixUnusedMetadataIsRejected) + TEST_METHOD(Validation_ControlInvalidPixMetadataIsRejected) dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -341,16 +343,83 @@ class PixTest : public ::testing::Test { pix_dxil::PixDxilInstNum::MDName, pix_dxil::PixDxilReg::MDName, pix_dxil::PixAllocaReg::MDName, pix_dxil::PixAllocaRegWrite::MDName}; - // Removes the known PIX metadata kinds from every function and instruction. + // Checks that known PIX metadata has the expected location and payload. + static bool hasValidKnownPixVirtualRegisterMetadata(llvm::Module &M) { + llvm::LLVMContext &Ctx = M.getContext(); + unsigned InstNumKindID = Ctx.getMDKindID(pix_dxil::PixDxilInstNum::MDName); + unsigned RegKindID = Ctx.getMDKindID(pix_dxil::PixDxilReg::MDName); + unsigned AllocaRegKindID = Ctx.getMDKindID(pix_dxil::PixAllocaReg::MDName); + unsigned AllocaRegWriteKindID = + Ctx.getMDKindID(pix_dxil::PixAllocaRegWrite::MDName); + + for (llvm::Function &F : M) { + for (const char *Kind : KnownPixVirtualRegisterMetadataKinds) { + if (F.getMetadata(Ctx.getMDKindID(Kind)) != nullptr) { + return false; + } + } + + for (llvm::BasicBlock &BB : F) { + for (llvm::Instruction &I : BB) { + if (I.getMetadata(InstNumKindID) != nullptr) { + std::uint32_t InstNum; + if (!pix_dxil::PixDxilInstNum::FromInst(&I, &InstNum)) { + return false; + } + } + + if (I.getMetadata(RegKindID) != nullptr) { + std::uint32_t RegNum; + if (!pix_dxil::PixDxilReg::FromInst(&I, &RegNum)) { + return false; + } + } + + if (I.getMetadata(AllocaRegKindID) != nullptr) { + llvm::AllocaInst *Alloca = llvm::dyn_cast(&I); + std::uint32_t RegBase; + std::uint32_t RegSize; + if (Alloca == nullptr || + !pix_dxil::PixAllocaReg::FromInst(Alloca, &RegBase, &RegSize)) { + return false; + } + } + + if (I.getMetadata(AllocaRegWriteKindID) != nullptr) { + llvm::StoreInst *Store = llvm::dyn_cast(&I); + std::uint32_t RegBase; + std::uint32_t RegSize; + llvm::Value *Index; + if (Store == nullptr || !pix_dxil::PixAllocaRegWrite::FromInst( + Store, &RegBase, &RegSize, &Index)) { + return false; + } + } + } + } + } + return true; + } + + // Removes valid PIX metadata from its documented instruction types. static void stripKnownPixVirtualRegisterMetadata(llvm::Module &M) { llvm::LLVMContext &Ctx = M.getContext(); - for (const char *Kind : KnownPixVirtualRegisterMetadataKinds) { - unsigned KindID = Ctx.getMDKindID(Kind); - for (llvm::Function &F : M) { - F.setMetadata(KindID, nullptr); - for (llvm::BasicBlock &BB : F) { - for (llvm::Instruction &I : BB) { - I.setMetadata(KindID, nullptr); + unsigned InstNumKindID = Ctx.getMDKindID(pix_dxil::PixDxilInstNum::MDName); + unsigned RegKindID = Ctx.getMDKindID(pix_dxil::PixDxilReg::MDName); + unsigned AllocaRegKindID = Ctx.getMDKindID(pix_dxil::PixAllocaReg::MDName); + unsigned AllocaRegWriteKindID = + Ctx.getMDKindID(pix_dxil::PixAllocaRegWrite::MDName); + + for (llvm::Function &F : M) { + for (llvm::BasicBlock &BB : F) { + for (llvm::Instruction &I : BB) { + I.setMetadata(InstNumKindID, nullptr); + I.setMetadata(RegKindID, nullptr); + if (llvm::isa(&I)) { + I.setMetadata(AllocaRegKindID, nullptr); + } + if (llvm::isa(&I)) { + I.setMetadata(AllocaRegWriteKindID, nullptr); } } } @@ -395,8 +464,17 @@ class PixTest : public ::testing::Test { // from any other unsupported metadata. Strip only the known PIX kinds // and revalidate. If this fixes the module, PIX metadata was the only // cause. - CComPtr StrippedContainer = - cloneModuleAndMutate(Container, stripKnownPixVirtualRegisterMetadata); + bool KnownPixMetadataIsValid = false; + CComPtr StrippedContainer = cloneModuleAndMutate( + Container, [&KnownPixMetadataIsValid](llvm::Module &M) { + KnownPixMetadataIsValid = hasValidKnownPixVirtualRegisterMetadata(M); + if (KnownPixMetadataIsValid) { + stripKnownPixVirtualRegisterMetadata(M); + } + }); + if (!KnownPixMetadataIsValid) { + return DirectResult; + } if (runValidator(StrippedContainer).Valid) { return {true, {}}; } @@ -3750,3 +3828,77 @@ float main() : SV_Target ValidationResult Result = validateInstrumentedModule(WithForeignMetadata); VERIFY_IS_FALSE(Result.Valid); } + +TEST_F(PixTest, Validation_ControlInvalidPixMetadataIsRejected) { + const char *Source = R"x( +float main() : SV_Target +{ + return 0; +})x"; + + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ps_6_0", {L"-Od"}); + SinglePassOutput Output = + runSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); + CComPtr Container = normalizeToContainer(Output.Module); + + bool AddedFunctionMetadata = false; + CComPtr WithFunctionMetadata = cloneModuleAndMutate( + Container, [&AddedFunctionMetadata](llvm::Module &M) { + for (llvm::Function &F : M) { + if (F.isDeclaration()) { + continue; + } + F.setMetadata(pix_dxil::PixDxilInstNum::MDName, + llvm::MDNode::get(M.getContext(), {})); + AddedFunctionMetadata = true; + break; + } + }); + VERIFY_IS_TRUE(AddedFunctionMetadata); + VERIFY_IS_FALSE(validateInstrumentedModule(WithFunctionMetadata).Valid); + + bool AddedMisplacedMetadata = false; + CComPtr WithMisplacedMetadata = cloneModuleAndMutate( + Container, [&AddedMisplacedMetadata](llvm::Module &M) { + llvm::LLVMContext &Ctx = M.getContext(); + llvm::Type *Int32Ty = llvm::Type::getInt32Ty(Ctx); + llvm::MDNode *ValidAllocaReg = llvm::MDNode::get( + Ctx, + {llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int32Ty, pix_dxil::PixAllocaReg::ID)), + llvm::ConstantAsMetadata::get(llvm::ConstantInt::get(Int32Ty, 0)), + llvm::ConstantAsMetadata::get( + llvm::ConstantInt::get(Int32Ty, 1))}); + for (llvm::Function &F : M) { + for (llvm::BasicBlock &BB : F) { + for (llvm::Instruction &I : BB) { + if (!llvm::isa(&I)) { + I.setMetadata(pix_dxil::PixAllocaReg::MDName, ValidAllocaReg); + AddedMisplacedMetadata = true; + return; + } + } + } + } + }); + VERIFY_IS_TRUE(AddedMisplacedMetadata); + VERIFY_IS_FALSE(validateInstrumentedModule(WithMisplacedMetadata).Valid); + + bool AddedMalformedMetadata = false; + CComPtr WithMalformedMetadata = cloneModuleAndMutate( + Container, [&AddedMalformedMetadata](llvm::Module &M) { + for (llvm::Function &F : M) { + for (llvm::BasicBlock &BB : F) { + for (llvm::Instruction &I : BB) { + I.setMetadata(pix_dxil::PixDxilInstNum::MDName, + llvm::MDNode::get(M.getContext(), {})); + AddedMalformedMetadata = true; + return; + } + } + } + }); + VERIFY_IS_TRUE(AddedMalformedMetadata); + VERIFY_IS_FALSE(validateInstrumentedModule(WithMalformedMetadata).Valid); +} From 1ccbb12608f2bcac95c987f1a28cf727f6408f0a Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 17 Sep 2026 11:45:36 -0700 Subject: [PATCH 23/39] [PIX] Report declaration cleanup mutations Return whether PIX operation declarations were erased and propagate that state through no-op pass paths and dynamic-resource traversal. Keep cleanup-only execution from emitting instrumentation diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DxilDebugBreakInstrumentation.cpp | 9 +++-- ...NonUniformResourceIndexInstrumentation.cpp | 11 +++---- .../DxilOutputColorBecomesConstant.cpp | 10 +++--- lib/DxilPIXPasses/DxilRemoveDiscards.cpp | 2 +- .../DxilShaderAccessTracking.cpp | 2 +- lib/DxilPIXPasses/PixPassHelpers.cpp | 33 +++++++++---------- lib/DxilPIXPasses/PixPassHelpers.h | 4 +-- tools/clang/unittests/HLSL/PixTest.cpp | 17 ++++++---- 8 files changed, 43 insertions(+), 45 deletions(-) diff --git a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp index 13cbf7c6c8..30fe00156f 100644 --- a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp @@ -120,15 +120,14 @@ bool DxilDebugBreakInstrumentation::runOnModule(Module &M) { CI->eraseFromParent(); } - PIXPassHelpers::eraseIfUnused(DM, DebugBreakFunc); - PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); - - const bool modified = (PixUAVResource != nullptr); + bool modified = (PixUAVResource != nullptr); + modified |= PIXPassHelpers::eraseIfUnused(DM, DebugBreakFunc); + modified |= PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); if (modified) { DM.ReEmitDxilResources(); - if (OSOverride != nullptr) { + if (OSOverride != nullptr && PixUAVResource != nullptr) { formatted_raw_ostream FOS(*OSOverride); FOS << "\nFoundDebugBreak\n"; } diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index cbeb778c41..d6c8a57059 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -64,7 +64,7 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { // then insert WaveActiveAllEqual to determine if the index is uniform // and finally write to a UAV resource with the result. - PIXPassHelpers::ForEachDynamicallyIndexedResource( + bool modified = PIXPassHelpers::ForEachDynamicallyIndexedResource( DM, [&](bool IsNonUniformIndex, Instruction *CreateHandle, Value *IndexOperand) { if (IsNonUniformIndex) { @@ -147,15 +147,14 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { return true; }); - const bool modified = (PixUAVResource != nullptr); - - PIXPassHelpers::eraseIfUnused(DM, WaveActiveAllEqualFunc); - PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); + modified |= (PixUAVResource != nullptr); + modified |= PIXPassHelpers::eraseIfUnused(DM, WaveActiveAllEqualFunc); + modified |= PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); if (modified) { DM.ReEmitDxilResources(); - if (OSOverride != nullptr) { + if (OSOverride != nullptr && PixUAVResource != nullptr) { formatted_raw_ostream FOS(*OSOverride); FOS << "\nFoundDynamicIndexingNoNuri\n"; } diff --git a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp index ff389f9348..7ecb712ea2 100644 --- a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp +++ b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp @@ -125,9 +125,9 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { [&hasIntOutputs](CallInst *) { hasIntOutputs = true; }); if (!hasFloatOutputs && !hasIntOutputs) { - PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); - PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); - return false; + bool Modified = PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); + Modified |= PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); + return Modified; } // Otherwise, we assume the shader outputs only one or the other (because the @@ -253,8 +253,8 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { }); } - PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); - PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); + Modified |= PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); + Modified |= PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); return Modified; } diff --git a/lib/DxilPIXPasses/DxilRemoveDiscards.cpp b/lib/DxilPIXPasses/DxilRemoveDiscards.cpp index 492dfebdb4..90db175e57 100644 --- a/lib/DxilPIXPasses/DxilRemoveDiscards.cpp +++ b/lib/DxilPIXPasses/DxilRemoveDiscards.cpp @@ -55,7 +55,7 @@ bool DxilRemoveDiscards::runOnModule(Module &M) { Modified = true; } - PIXPassHelpers::eraseIfUnused(DM, DiscardFunction); + Modified |= PIXPassHelpers::eraseIfUnused(DM, DiscardFunction); return Modified; } diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index 1dddb6c0e6..ed1d1b26cc 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -808,7 +808,7 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { bool FoundDynamicIndexing = false; - PIXPassHelpers::ForEachDynamicallyIndexedResource( + Modified |= PIXPassHelpers::ForEachDynamicallyIndexedResource( DM, [&FoundDynamicIndexing](bool, Instruction *, Value *) { FoundDynamicIndexing = true; return false; diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index c16d8a2fa3..e719a52ec0 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -385,11 +385,13 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, return ret; } -void eraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction) { +bool eraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction) { if (OpFunction != nullptr && OpFunction->user_empty()) { DM.GetOP()->RemoveFunction(OpFunction); OpFunction->eraseFromParent(); + return true; } + return false; } // Set up a UAV with structure of a single int @@ -539,7 +541,7 @@ unsigned int FindOrAddSV_Position(hlsl::DxilModule &DM, } } -void ForEachDynamicallyIndexedResource( +bool ForEachDynamicallyIndexedResource( hlsl::DxilModule &DM, const std::function &Visitor) { OP *HlslOP = DM.GetOP(); @@ -561,7 +563,7 @@ void ForEachDynamicallyIndexedResource( if (auto *gep = dyn_cast(resOrGep)) { if (!Visitor(DxilMDHelper::IsMarkedNonUniform(gep), load, gep->getOperand(2))) { - return; + return false; } } } @@ -578,18 +580,12 @@ void ForEachDynamicallyIndexedResource( llvm::Function *CreateHandleFromHeapFn = HlslOP->GetOpFunc( DXIL::OpCode::CreateHandleFromHeap, Type::getVoidTy(Ctx)); - struct UnusedDeclarationCleanup { - hlsl::DxilModule &DM; - llvm::Function *CreateHandleFn; - llvm::Function *CreateHandleFromBindingFn; - llvm::Function *CreateHandleFromHeapFn; - ~UnusedDeclarationCleanup() { - eraseIfUnused(DM, CreateHandleFn); - eraseIfUnused(DM, CreateHandleFromBindingFn); - eraseIfUnused(DM, CreateHandleFromHeapFn); - } - } Cleanup{DM, CreateHandleFn, CreateHandleFromBindingFn, - CreateHandleFromHeapFn}; + auto CleanupUnusedDeclarations = [&]() { + bool Modified = eraseIfUnused(DM, CreateHandleFn); + Modified |= eraseIfUnused(DM, CreateHandleFromBindingFn); + Modified |= eraseIfUnused(DM, CreateHandleFromHeapFn); + return Modified; + }; for (auto FI = CreateHandleFn->user_begin(); FI != CreateHandleFn->user_end();) { @@ -601,7 +597,7 @@ void ForEachDynamicallyIndexedResource( const DxilInst_CreateHandle createHandle(instruction); if (!Visitor(createHandle.get_nonUniformIndex_val(), instruction, index)) { - return; + return CleanupUnusedDeclarations(); } } } @@ -616,7 +612,7 @@ void ForEachDynamicallyIndexedResource( const DxilInst_CreateHandleFromBinding createHandle(instruction); if (!Visitor(createHandle.get_nonUniformIndex_val(), instruction, index)) { - return; + return CleanupUnusedDeclarations(); } } } @@ -631,10 +627,11 @@ void ForEachDynamicallyIndexedResource( const DxilInst_CreateHandleFromHeap createHandle(instruction); if (!Visitor(createHandle.get_nonUniformIndex_val(), instruction, index)) { - return; + return CleanupUnusedDeclarations(); } } } + return CleanupUnusedDeclarations(); } #ifdef PIX_DEBUG_DUMP_HELPER diff --git a/lib/DxilPIXPasses/PixPassHelpers.h b/lib/DxilPIXPasses/PixPassHelpers.h index 5efdc4940c..7dc13e1b7e 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.h +++ b/lib/DxilPIXPasses/PixPassHelpers.h @@ -48,7 +48,7 @@ llvm::CallInst *CreateHandleForResource(hlsl::DxilModule &DM, hlsl::DxilResourceBase *resource, const char *name); llvm::Function *GetEntryFunction(hlsl::DxilModule &DM); -void eraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction); +bool eraseIfUnused(hlsl::DxilModule &DM, llvm::Function *OpFunction); std::vector GetAllInstrumentableFunctions(hlsl::DxilModule &DM); hlsl::DXIL::ShaderKind GetFunctionShaderKind(hlsl::DxilModule &DM, @@ -83,7 +83,7 @@ void ReplaceAllUsesOfInstructionWithNewValueAndDeleteInstruction( llvm::Instruction *Instr, llvm::Value *newValue, llvm::Type *newType); unsigned int FindOrAddSV_Position(hlsl::DxilModule &DM, unsigned UpStreamSVPosRow); -void ForEachDynamicallyIndexedResource( +bool ForEachDynamicallyIndexedResource( hlsl::DxilModule &DM, const std::function &Visitor); diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 0d8ce95081..4ee97b9d1c 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -3702,7 +3702,7 @@ float4 main() : SV_Target VERIFY_ARE_EQUAL(1u, static_cast( HlslOP->GetOpFuncList(DXIL::OpCode::Discard).size())); - PIXPassHelpers::eraseIfUnused(DM, Discard); + VERIFY_IS_TRUE(PIXPassHelpers::eraseIfUnused(DM, Discard)); VERIFY_ARE_EQUAL(0u, static_cast( HlslOP->GetOpFuncList(DXIL::OpCode::Discard).size())); @@ -3711,7 +3711,8 @@ float4 main() : SV_Target HlslOP->GetOpFunc(DXIL::OpCode::Discard, llvm::Type::getVoidTy(DM.GetModule()->getContext())); VERIFY_IS_NOT_NULL(Recreated); - PIXPassHelpers::eraseIfUnused(DM, Recreated); + VERIFY_IS_TRUE(PIXPassHelpers::eraseIfUnused(DM, Recreated)); + VERIFY_IS_FALSE(PIXPassHelpers::eraseIfUnused(DM, nullptr)); } TEST_F(PixTest, DynamicResourceCleanup_VisitorStopsEarly) { @@ -3728,13 +3729,15 @@ float4 main(float2 uv : TEXCOORD0) : SV_Target ModuleAndHangersOn ModuleEtc(Compiled); DxilModule &DM = ModuleEtc.GetDxilModule(); bool VisitorCalled = false; - PIXPassHelpers::ForEachDynamicallyIndexedResource( - DM, [&VisitorCalled](bool, llvm::Instruction *, llvm::Value *) { - VisitorCalled = true; - return false; - }); + const bool DeclarationsRemoved = + PIXPassHelpers::ForEachDynamicallyIndexedResource( + DM, [&VisitorCalled](bool, llvm::Instruction *, llvm::Value *) { + VisitorCalled = true; + return false; + }); VERIFY_IS_TRUE(VisitorCalled); + VERIFY_IS_TRUE(DeclarationsRemoved); OP *HlslOP = DM.GetOP(); VERIFY_ARE_EQUAL( 0u, From 947c113e08cc2bb630f69c4215a3c2aae38d4043 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 17 Sep 2026 11:46:44 -0700 Subject: [PATCH 24/39] [PIX] Remove unused mesh index declaration Keep the materialized emitIndices function and erase it when a valid zero-output mesh shader has no index calls. Add a validating regression for that path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...DxilPIXMeshShaderOutputInstrumentation.cpp | 13 ++++---- tools/clang/unittests/HLSL/PixTest.cpp | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp b/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp index de04a246e1..457912f65d 100644 --- a/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilPIXMeshShaderOutputInstrumentation.cpp @@ -355,8 +355,9 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { FirstNewStructGetMeshPayload); } - auto F = HlslOP->GetOpFunc(DXIL::OpCode::EmitIndices, Type::getVoidTy(Ctx)); - auto FunctionUses = F->uses(); + Function *EmitIndicesFunction = + HlslOP->GetOpFunc(DXIL::OpCode::EmitIndices, Type::getVoidTy(Ctx)); + auto FunctionUses = EmitIndicesFunction->uses(); for (auto FI = FunctionUses.begin(); FI != FunctionUses.end();) { auto &FunctionUse = *FI++; auto FunctionUser = FunctionUse.getUser(); @@ -384,9 +385,10 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { SmallVector StoreVertexOutputFunctions; for (auto const &Overload : StoreVertexOutputOverloads) { - F = HlslOP->GetOpFunc(DXIL::OpCode::StoreVertexOutput, Overload.type); - StoreVertexOutputFunctions.push_back(F); - FunctionUses = F->uses(); + Function *StoreVertexOutputFunction = + HlslOP->GetOpFunc(DXIL::OpCode::StoreVertexOutput, Overload.type); + StoreVertexOutputFunctions.push_back(StoreVertexOutputFunction); + FunctionUses = StoreVertexOutputFunction->uses(); for (auto FI = FunctionUses.begin(); FI != FunctionUses.end();) { auto &FunctionUse = *FI++; auto FunctionUser = FunctionUse.getUser(); @@ -427,6 +429,7 @@ bool DxilPIXMeshShaderOutputInstrumentation::runOnModule(Module &M) { for (Function *StoreVertexOutputFunction : StoreVertexOutputFunctions) { PIXPassHelpers::eraseIfUnused(DM, StoreVertexOutputFunction); } + PIXPassHelpers::eraseIfUnused(DM, EmitIndicesFunction); DM.ReEmitDxilResources(); diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 4ee97b9d1c..fb97d3b438 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -159,6 +159,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(RemoveDiscards_UnusedDiscardOverloadIsErased) TEST_METHOD(OperationCacheCleanup_RemovesErasedFunctions) TEST_METHOD(DynamicResourceCleanup_VisitorStopsEarly) + TEST_METHOD(MeshOutput_NoIndicesDeclarationIsErased) TEST_METHOD(DxilPIXDXRInvocationsLog_SanityTest) TEST_METHOD(DxilPIXDXRInvocationsLog_EmbeddedRootSigs) @@ -3749,6 +3750,35 @@ float4 main(float2 uv : TEXCOORD0) : SV_Target HlslOP->GetOpFuncList(DXIL::OpCode::CreateHandleFromHeap).size())); } +TEST_F(PixTest, MeshOutput_NoIndicesDeclarationIsErased) { + const char *Source = R"x( +struct Vertex +{ + float4 Position : SV_Position; +}; + +[outputtopology("point")] +[numthreads(1, 1, 1)] +void main(out vertices Vertex Vertices[1], + out indices uint3 Indices[1]) +{ + Vertices[0].Position = 0; + SetMeshOutputCounts(0, 0); +})x"; + + CComPtr Compiled = + Compile(m_dllSupport, Source, L"ms_6_5", {L"-Od"}); + SinglePassOutput Output = runSinglePass( + Compiled, + L"-hlsl-dxil-pix-meshshader-output-instrumentation,expand-payload=0," + L"UAVSize=8192"); + const std::string Disassembly = Disassemble(Output.Module); + + VERIFY_IS_FALSE(hasDeclaration(Disassembly, "dx.op.emitIndices")); + verifyInstrumentedModuleIsValid(Output.Module, + "mesh output with no emitted indices"); +} + TEST_F(PixTest, DxilPIXDXRInvocationsLog_SanityTest) { const char *source = R"x( From 3fd13a26e6cbc14750329214dae25ef5aea8aa16 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 17 Sep 2026 11:47:37 -0700 Subject: [PATCH 25/39] [PIX] Collect final NURI shader flags Recompute shader flags after non-uniform-index instrumentation and before metadata emission. Normalize bare optimizer bitcode to assert serialized WaveOps and validate unqualified paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DxilNonUniformResourceIndexInstrumentation.cpp | 1 + tools/clang/unittests/HLSL/PixTest.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index d6c8a57059..97342a0499 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -152,6 +152,7 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { modified |= PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); if (modified) { + DM.CollectShaderFlagsForModule(); DM.ReEmitDxilResources(); if (OSOverride != nullptr && PixUAVResource != nullptr) { diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index fb97d3b438..e0fb21a587 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -3877,6 +3877,14 @@ void PixTest::TestNuriCase(const char *source, const wchar_t *target, const std::vector &dxilLines = Output.lines; VERIFY_ARE_EQUAL(NuriGetWaveInstructionCount(dxilLines), expectedResult); + if (expectedResult != 0) { + CComPtr Container = normalizeToContainer(Output.blob); + ModuleAndHangersOn OutputModule(Container); + VERIFY_IS_TRUE(OutputModule.GetDxilModule().m_ShaderFlags.GetWaveOps()); + verifyInstrumentedModuleIsValid( + Output.blob, + "unqualified non-uniform resource index instrumentation"); + } bool foundDynamicIndexingNoNuri = false; const std::vector outputTextLines = Tokenize(outputText, "\n"); From 29e29d277acf5b86bf3ec05850a5e5acf82cb381 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 17 Sep 2026 17:34:34 -0700 Subject: [PATCH 26/39] [PIX] Preserve root signature source text Carry an owned copy of optional global root-signature source text while replacing subobjects, preserving the distinction between absent and present text. Add in-memory and emitted-metadata coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/DxilPIXPasses/PixPassHelpers.cpp | 17 ++++- tools/clang/unittests/HLSL/PixTest.cpp | 92 ++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index e719a52ec0..a3e463a5fe 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -26,6 +26,7 @@ #include "dxc/Support/Global.h" #include "dxc/Support/WinIncludes.h" #include "dxc/dxcapi.h" +#include #ifdef PIX_DEBUG_DUMP_HELPER #include "llvm/IR/DebugInfo.h" @@ -272,6 +273,7 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, struct ReplacementRootSignature { std::string Name; std::vector Data; + std::optional Text; }; std::vector ReplacementRootSignatures; @@ -282,14 +284,19 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, DXIL::SubobjectKind::GlobalRootSignature) { const void *Data = nullptr; uint32_t Size = 0; + const char *Text = nullptr; constexpr bool notALocalRS = false; if (subObject.second->GetRootSignature(notALocalRS, Data, Size, - nullptr)) { + &Text)) { std::vector ExtendedRootSignature = AddUAVParamterToRootSignature(Data, Size, ToolsUAVRegister); if (!ExtendedRootSignature.empty()) { + std::optional OwnedText; + if (Text != nullptr) + OwnedText = Text; ReplacementRootSignatures.push_back( - {subObject.first.str(), std::move(ExtendedRootSignature)}); + {subObject.first.str(), std::move(ExtendedRootSignature), + std::move(OwnedText)}); } } } @@ -299,9 +306,13 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, for (const ReplacementRootSignature &Replacement : ReplacementRootSignatures) { subObjects->RemoveSubobject(Replacement.Name); + std::optional TextRef; + if (Replacement.Text) + TextRef.emplace(*Replacement.Text); subObjects->CreateRootSignature( Replacement.Name, NotALocalRootSignature, Replacement.Data.data(), - static_cast(Replacement.Data.size())); + static_cast(Replacement.Data.size()), + TextRef ? &*TextRef : nullptr); } } } diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index e0fb21a587..761f7963c6 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -151,6 +151,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(ToolsUav_TwoPixPassesShareOneResource) TEST_METHOD(ToolsUav_LibraryWithTwoEntryPointsCreatesOnePair) TEST_METHOD(ToolsUav_ExtendsEveryGlobalRootSignatureSubobject) + TEST_METHOD(ToolsUav_PreservesGlobalRootSignatureSourceText) TEST_METHOD(DebugInstrumentation_RawBufferShaderFlagDeclared) TEST_METHOD(ToolsUav_RootSignatureSerializationFailurePreservesSignature) TEST_METHOD(ToolsUav_ExtendingRootSignaturePreservesUnrelatedParameterFlags) @@ -3395,6 +3396,97 @@ void MyMiss(inout MyPayload payload) {0, 1}); } +TEST_F(PixTest, ToolsUav_PreservesGlobalRootSignatureSourceText) { + const char *Source = R"x( +struct Payload +{ + float4 Color; +}; + +[shader("miss")] +void main(inout Payload Value) +{ +})x"; + + DxilRootParameter Parameter = {}; + Parameter.ParameterType = DxilRootParameterType::CBV; + Parameter.Descriptor.ShaderRegister = 0; + Parameter.ShaderVisibility = DxilShaderVisibility::All; + + DxilVersionedRootSignatureDesc RootSignature = {}; + RootSignature.Version = DxilRootSignatureVersion::Version_1_0; + RootSignature.Desc_1_0.NumParameters = 1; + RootSignature.Desc_1_0.pParameters = &Parameter; + RootSignature.Desc_1_0.Flags = DxilRootSignatureFlags::None; + + CComPtr SerializedRootSignature; + CComPtr ErrorBlob; + SerializeRootSignature(&RootSignature, &SerializedRootSignature, &ErrorBlob, + true); + VERIFY_IS_NOT_NULL(SerializedRootSignature); + + CComPtr Compiled = Compile(m_dllSupport, Source, L"lib_6_6", {}); + ModuleAndHangersOn ModuleEtc(Compiled); + DxilModule &DM = ModuleEtc.GetDxilModule(); + + std::unique_ptr Subobjects(new DxilSubobjects()); + const std::string ExpectedText = "CBV(b0)"; + llvm::StringRef TextRef(ExpectedText); + constexpr bool NotALocalRootSignature = false; + Subobjects->CreateRootSignature( + "testRootSignature", NotALocalRootSignature, + SerializedRootSignature->GetBufferPointer(), + static_cast(SerializedRootSignature->GetBufferSize()), + &TextRef); + DM.ResetSubobjects(Subobjects.release()); + + PIXPassHelpers::CreateGlobalUAVResource(DM, 0, "PIX_TestUAV"); + + auto VerifyTextAndUAV = [&](DxilModule &Module) { + DxilSubobjects *ModuleSubobjects = Module.GetSubobjects(); + VERIFY_IS_NOT_NULL(ModuleSubobjects); + if (ModuleSubobjects == nullptr) + return; + DxilSubobject *Subobject = + ModuleSubobjects->FindSubobject("testRootSignature"); + VERIFY_IS_NOT_NULL(Subobject); + const void *Data = nullptr; + uint32_t Size = 0; + const char *Text = nullptr; + VERIFY_IS_TRUE( + Subobject->GetRootSignature(NotALocalRootSignature, Data, Size, &Text)); + VERIFY_IS_NOT_NULL(Text); + VERIFY_ARE_EQUAL(ExpectedText, std::string(Text)); + + DxilVersionedRootSignatureDesc const *UpdatedRootSignature = nullptr; + DeserializeRootSignature(Data, Size, &UpdatedRootSignature); + VERIFY_IS_TRUE(rootSignatureHasToolsUAV(UpdatedRootSignature, 0)); + DeleteRootSignature(UpdatedRootSignature); + }; + + VerifyTextAndUAV(DM); + + DM.ReEmitDxilResources(); + llvm::NamedMDNode *SubobjectsMetadata = + DM.GetModule()->getNamedMetadata(DxilMDHelper::kDxilSubobjectsMDName); + VERIFY_IS_NOT_NULL(SubobjectsMetadata); + bool FoundMetadata = false; + for (unsigned Index = 0; Index < SubobjectsMetadata->getNumOperands(); + ++Index) { + llvm::MDNode *Entry = SubobjectsMetadata->getOperand(Index); + llvm::MDString *Name = llvm::dyn_cast(Entry->getOperand(0)); + if (Name == nullptr || Name->getString() != "testRootSignature") + continue; + llvm::MDString *Text = llvm::dyn_cast(Entry->getOperand(3)); + VERIFY_IS_NOT_NULL(Text); + if (Text != nullptr) + VERIFY_ARE_EQUAL(ExpectedText, Text->getString().str()); + FoundMetadata = true; + break; + } + VERIFY_IS_TRUE(FoundMetadata); +} + TEST_F(PixTest, DebugInstrumentation_RawBufferShaderFlagDeclared) { const char *Source = R"x( [numthreads(1, 1, 1)] From f0c9fd1c8807711ddabbde5d656aebed35ff1386 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 28 Sep 2026 15:24:39 -0700 Subject: [PATCH 27/39] [NFC] Omit braces for single-statement PIX conditions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d08bc707-8369-41b0-b88d-9b0c997a538b --- lib/DxilPIXPasses/PixPassHelpers.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index a3e463a5fe..201befc6b7 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -186,9 +186,8 @@ static std::vector SerializeRootSignatureToVector( SerializeRootSignature(rootSignature, &serializedRootSignature, &errorBlob, allowReservedRegisterSpace); std::vector ret; - if (serializedRootSignature == nullptr) { + if (serializedRootSignature == nullptr) return ret; - } auto const *serializedData = reinterpret_cast( serializedRootSignature->GetBufferPointer()); ret.assign(serializedData, @@ -247,10 +246,9 @@ AddUAVParamterToRootSignature(const void *Data, uint32_t Size, break; case DxilRootSignatureVersion::Version_1_1: if (ExtendRootSig( - rs->Desc_1_1, ToolsUAVRegister)) { + rs->Desc_1_1, ToolsUAVRegister)) rs->Desc_1_1.pParameters[rs->Desc_1_1.NumParameters - 1] .Descriptor.Flags = hlsl::DxilRootDescriptorFlags::None; - } break; } return SerializeRootSignatureToVector(rs); @@ -262,9 +260,8 @@ static void AddUAVToShaderAttributeRootSignature(DxilModule &DM, if (!rs.empty()) { std::vector asVector = AddUAVParamterToRootSignature( rs.data(), static_cast(rs.size()), ToolsUAVRegister); - if (!asVector.empty()) { + if (!asVector.empty()) DM.ResetSerializedRootSignature(asVector); - } } } @@ -325,9 +322,8 @@ hlsl::DxilResource *CreateGlobalUAVResource(hlsl::DxilModule &DM, for (const std::unique_ptr &ExistingUAV : DM.GetUAVs()) { if (ExistingUAV->GetSpaceID() == toolsRegisterSpace && - ExistingUAV->GetLowerBound() == hlslBindIndex) { + ExistingUAV->GetLowerBound() == hlslBindIndex) return ExistingUAV.get(); - } } const char *PIXStructTypeName = ShaderModelHandleTypeName(DM); From e8a7ca974f68b28cc47cc83410523fb9356e2560 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Mon, 28 Sep 2026 18:42:18 -0700 Subject: [PATCH 28/39] [PIX] Preserve L3 helper naming after merge Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/clang/unittests/HLSL/PixTest.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 691d728418..d26dab0dd9 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -3984,11 +3984,11 @@ TEST_F(PixTest, DxilPIXDXRInvocationsLog_ZeroCapacityEmitsNothing) { auto oneEntryOutput = RunDxilPIXDXRInvocationsLog(compiledLib, 1); auto oneEntryLines = Tokenize(Disassemble(oneEntryOutput), "\n"); - VERIFY_ARE_EQUAL(2, CountToolsUAVRecords(oneEntryLines)); + VERIFY_ARE_EQUAL(2, countToolsUAVRecords(oneEntryLines)); auto zeroEntryOutput = RunDxilPIXDXRInvocationsLog(compiledLib, 0); auto zeroEntryLines = Tokenize(Disassemble(zeroEntryOutput), "\n"); - VERIFY_ARE_EQUAL(0, CountToolsUAVRecords(zeroEntryLines)); + VERIFY_ARE_EQUAL(0, countToolsUAVRecords(zeroEntryLines)); } TEST_F(PixTest, DxilPIXDXRInvocationsLog_OneEntryUsesEntryCountBound) { @@ -4016,7 +4016,7 @@ TEST_F(PixTest, DxilPIXDXRInvocationsLog_OverflowGuardValidates) { std::string disassembly = Disassemble(output); VERIFY_IS_TRUE(disassembly.find("@dx.op.binary.i32") == std::string::npos); - VerifyInstrumentedModuleIsValid(output, "DXR invocations log overflow guard"); + verifyInstrumentedModuleIsValid(output, "DXR invocations log overflow guard"); } uint32_t NuriGetWaveInstructionCount(const std::vector &lines) { From fddd3bddf081a2b835cbf17a6508ff63dc07b96b Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 29 Sep 2026 09:16:01 -0700 Subject: [PATCH 29/39] [PIX] Apply clang-format to PixTest.cpp Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/clang/unittests/HLSL/PixTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index a9fc8fc01b..fa2465c6cb 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -4821,8 +4821,8 @@ float4 main(float4 pos : SV_Position) : SV_Target VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); - verifyInstrumentedModuleIsValid( - pOptimizedModule, "non-uniform resource index instrumentation"); + verifyInstrumentedModuleIsValid(pOptimizedModule, + "non-uniform resource index instrumentation"); VERIFY_ARE_NOT_EQUAL( std::string::npos, From 683dcf767620ff22e89adc1928f5036607e2ed3e Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 29 Sep 2026 09:32:05 -0700 Subject: [PATCH 30/39] [PIX] Guard winerror.h include in pixel-hit instrumentation for non-Windows builds Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp b/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp index 00f68ce379..a1f6affdee 100644 --- a/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilAddPixelHitInstrumentation.cpp @@ -24,7 +24,9 @@ #include "PixPassHelpers.h" #include "dxc/Support/Global.h" +#ifdef _WIN32 #include +#endif using namespace llvm; using namespace hlsl; From 7032f2eb68942e030c281748d7dcaa7d4bcc7f73 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 29 Sep 2026 11:56:31 -0700 Subject: [PATCH 31/39] [PIX] Link DxilPIXPasses against HLSL for DxilSignatureAllocator Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/DxilPIXPasses/LLVMBuild.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DxilPIXPasses/LLVMBuild.txt b/lib/DxilPIXPasses/LLVMBuild.txt index 20428b278b..a41faeec6f 100644 --- a/lib/DxilPIXPasses/LLVMBuild.txt +++ b/lib/DxilPIXPasses/LLVMBuild.txt @@ -13,4 +13,4 @@ type = Library name = DxilPIXPasses parent = Libraries -required_libraries = BitReader Core DxcSupport TransformUtils Support +required_libraries = BitReader Core DxcSupport HLSL TransformUtils Support From 0a691ffac6df08ccd7037aed76d506f9d474c09f Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 29 Sep 2026 14:20:52 -0700 Subject: [PATCH 32/39] [PIX] Don't leak a pass whose applyOptions throws DxcOptimizer::RunOptimizer created each pass as a raw pointer and called setOSOverride/applyOptions before handing it to the pass manager, so a pass that rejects its options by throwing (e.g. pixel-hit instrumentation with unrepresentable dimensions) was leaked. Hold it in a unique_ptr until it is added, and read its kind before ownership is transferred. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/HLSL/DxcOptimizer.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/HLSL/DxcOptimizer.cpp b/lib/HLSL/DxcOptimizer.cpp index 772cfcc3db..a035c874cf 100644 --- a/lib/HLSL/DxcOptimizer.cpp +++ b/lib/HLSL/DxcOptimizer.cpp @@ -43,6 +43,7 @@ #include #include // should change this for string_table +#include #include #include "llvm/PassPrinters/PassPrinters.h" @@ -518,14 +519,16 @@ HRESULT STDMETHODCALLTYPE DxcOptimizer::RunOptimizer( DXASSERT(PassInf->getNormalCtor(), "else pass with no default .ctor was added"); - Pass *pass = PassInf->getNormalCtor()(); + // Own the pass until the pass manager takes it, so it isn't leaked if + // applyOptions throws. + std::unique_ptr pass(PassInf->getNormalCtor()()); pass->setOSOverride(&outStream); pass->applyOptions(options); options.clear(); - pPassManager->add(pass); + const PassKind Kind = pass->getPassKind(); + pPassManager->add(pass.release()); if (AnalyzeOnly) { const bool Quiet = false; - PassKind Kind = pass->getPassKind(); switch (Kind) { case PT_BasicBlock: pPassManager->add( From 00edebea9a15b96a598e06a5e12c6f475e72a9a7 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Tue, 29 Sep 2026 16:40:48 -0700 Subject: [PATCH 33/39] [PIX] Address review feedback: explicit DebugBreak tracking, typo, test helper - DxilDebugBreakInstrumentation: introduce an explicit FoundDebugBreak flag instead of using the UAV resource pointer as a proxy for whether a DebugBreak call was found. Only call ReEmitDxilResources and emit the FoundDebugBreak marker when a DebugBreak was actually instrumented, independent of whether unused op declarations were also erased. - PixPassHelpers: fix typo AddUAVParamterToRootSignature -> AddUAVParameterToRootSignature (definition and call sites). - PixTest: refactor rootSignatureHasToolsUAV to share a single generic lambda between the 1.0 and 1.1 root signature versions, using a named ToolsRegisterSpace constant instead of a magic number. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DxilDebugBreakInstrumentation.cpp | 14 ++++--- lib/DxilPIXPasses/PixPassHelpers.cpp | 8 ++-- tools/clang/unittests/HLSL/PixTest.cpp | 37 ++++++++----------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp index 30fe00156f..8c90074d91 100644 --- a/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugBreakInstrumentation.cpp @@ -120,20 +120,22 @@ bool DxilDebugBreakInstrumentation::runOnModule(Module &M) { CI->eraseFromParent(); } - bool modified = (PixUAVResource != nullptr); - modified |= PIXPassHelpers::eraseIfUnused(DM, DebugBreakFunc); - modified |= PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); + const bool FoundDebugBreak = !DebugBreakCalls.empty(); - if (modified) { + bool Modified = FoundDebugBreak; + Modified |= PIXPassHelpers::eraseIfUnused(DM, DebugBreakFunc); + Modified |= PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); + + if (FoundDebugBreak) { DM.ReEmitDxilResources(); - if (OSOverride != nullptr && PixUAVResource != nullptr) { + if (OSOverride != nullptr) { formatted_raw_ostream FOS(*OSOverride); FOS << "\nFoundDebugBreak\n"; } } - return modified; + return Modified; } char DxilDebugBreakInstrumentation::ID = 0; diff --git a/lib/DxilPIXPasses/PixPassHelpers.cpp b/lib/DxilPIXPasses/PixPassHelpers.cpp index 201befc6b7..472cf77e79 100644 --- a/lib/DxilPIXPasses/PixPassHelpers.cpp +++ b/lib/DxilPIXPasses/PixPassHelpers.cpp @@ -234,8 +234,8 @@ bool ExtendRootSig(RootSigDesc &RootSignatureDesc, uint32_t ToolsUAVRegister) { } static std::vector -AddUAVParamterToRootSignature(const void *Data, uint32_t Size, - uint32_t ToolsUAVRegister) { +AddUAVParameterToRootSignature(const void *Data, uint32_t Size, + uint32_t ToolsUAVRegister) { DxilVersionedRootSignature rootSignature; DeserializeRootSignature(Data, Size, rootSignature.get_address_of()); auto *rs = rootSignature.get_mutable(); @@ -258,7 +258,7 @@ static void AddUAVToShaderAttributeRootSignature(DxilModule &DM, uint32_t ToolsUAVRegister) { auto rs = DM.GetSerializedRootSignature(); if (!rs.empty()) { - std::vector asVector = AddUAVParamterToRootSignature( + std::vector asVector = AddUAVParameterToRootSignature( rs.data(), static_cast(rs.size()), ToolsUAVRegister); if (!asVector.empty()) DM.ResetSerializedRootSignature(asVector); @@ -286,7 +286,7 @@ static void AddUAVToDxilDefinedGlobalRootSignatures(DxilModule &DM, if (subObject.second->GetRootSignature(notALocalRS, Data, Size, &Text)) { std::vector ExtendedRootSignature = - AddUAVParamterToRootSignature(Data, Size, ToolsUAVRegister); + AddUAVParameterToRootSignature(Data, Size, ToolsUAVRegister); if (!ExtendedRootSignature.empty()) { std::optional OwnedText; if (Text != nullptr) diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index 761f7963c6..6552d3643f 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -790,37 +790,30 @@ static int countToolsUAVRecords(std::vector const &Lines) { return Count; } +static constexpr uint32_t ToolsRegisterSpace = static_cast(-2); + static bool rootSignatureHasToolsUAV(const DxilVersionedRootSignatureDesc *RootSignature, uint32_t ShaderRegister) { - switch (RootSignature->Version) { - case DxilRootSignatureVersion::Version_1_0: { - const DxilRootSignatureDesc &Desc = RootSignature->Desc_1_0; - for (uint32_t ParameterIndex = 0; ParameterIndex < Desc.NumParameters; - ++ParameterIndex) { - const DxilRootParameter &Parameter = Desc.pParameters[ParameterIndex]; - if (Parameter.ParameterType == DxilRootParameterType::UAV && - Parameter.Descriptor.RegisterSpace == static_cast(-2) && - Parameter.Descriptor.ShaderRegister == ShaderRegister) { - return true; - } - } - break; - } - case DxilRootSignatureVersion::Version_1_1: { - const DxilRootSignatureDesc1 &Desc = RootSignature->Desc_1_1; + auto HasToolsUAV = [ShaderRegister](const auto &Desc) { for (uint32_t ParameterIndex = 0; ParameterIndex < Desc.NumParameters; ++ParameterIndex) { - const DxilRootParameter1 &Parameter = Desc.pParameters[ParameterIndex]; + const auto &Parameter = Desc.pParameters[ParameterIndex]; if (Parameter.ParameterType == DxilRootParameterType::UAV && - Parameter.Descriptor.RegisterSpace == static_cast(-2) && - Parameter.Descriptor.ShaderRegister == ShaderRegister) { + Parameter.Descriptor.RegisterSpace == ToolsRegisterSpace && + Parameter.Descriptor.ShaderRegister == ShaderRegister) return true; - } } - break; - } + return false; + }; + + switch (RootSignature->Version) { + case DxilRootSignatureVersion::Version_1_0: + return HasToolsUAV(RootSignature->Desc_1_0); + case DxilRootSignatureVersion::Version_1_1: + return HasToolsUAV(RootSignature->Desc_1_1); } + return false; } From e76a6d77a0a5b0edbfc5c41acb03092f23221424 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 1 Oct 2026 14:52:17 -0700 Subject: [PATCH 34/39] [PIX] Recompute shader flags after NURI instrumentation The non-uniform resource index pass inserts WaveActiveAllEqual after the shader flags were computed, so the module could contain wave operations it does not declare and fail validation. Recompute the flags when the pass changes the module. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- ...NonUniformResourceIndexInstrumentation.cpp | 2 + tools/clang/unittests/HLSL/PixTest.cpp | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp index 65be5e44b5..ea35002614 100644 --- a/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilNonUniformResourceIndexInstrumentation.cpp @@ -160,6 +160,8 @@ bool DxilNonUniformResourceIndexInstrumentation::runOnModule(Module &M) { modified |= PIXPassHelpers::eraseIfUnused(DM, AtomicOpFunc); if (modified) { + // Recompute shader flags after inserting WaveActiveAllEqual so the + // declared flags match the module. DM.CollectShaderFlagsForModule(); DM.ReEmitDxilResources(); diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index eb466f41ec..c99b249b0f 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -190,6 +190,8 @@ class PixTest : public ::testing::Test { TEST_METHOD(Validation_ControlInvalidModuleFails) TEST_METHOD(Validation_ControlNonPixUnusedMetadataIsRejected) TEST_METHOD(Validation_ControlInvalidPixMetadataIsRejected) + TEST_METHOD(Validation_ControlBoilerplateOnlyFailureIsRejected) + TEST_METHOD(Validation_NonUniformResourceIndex_WaveOpsFlag) dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -4625,6 +4627,13 @@ float main() : SV_Target SinglePassOutput Output = runSinglePass(Compiled, L"-dxil-annotate-with-virtual-regs"); + // Confirm the baseline validates before corrupting it, so the failure + // below is caused by the corruption and nothing else. + verifyInstrumentedModuleIsValid( + Output.Module, + "virtual-register annotation of a trivial pixel shader, uncorrupted " + "baseline (validation harness control)"); + // Mislabel the shader stage, so the container carries both the // harness's permitted PIX metadata and a real defect. std::string Disassembly = Disassemble(Output.Module); @@ -4768,3 +4777,59 @@ float main() : SV_Target VERIFY_IS_TRUE(AddedMalformedMetadata); VERIFY_IS_FALSE(validateInstrumentedModule(WithMalformedMetadata).Valid); } + +TEST_F(PixTest, Validation_ControlBoilerplateOnlyFailureIsRejected) { + const std::string boilerplateOnly = + getSignificantValidationDiagnostics("Validation failed.\n"); + VERIFY_IS_TRUE(boilerplateOnly.empty()); + + const std::string realDiagnostic = + getSignificantValidationDiagnostics("Validation failed.\n" + "Some real validator diagnostic.\n"); + VERIFY_IS_FALSE(realDiagnostic.empty()); + VERIFY_IS_TRUE(realDiagnostic.find("Some real validator diagnostic.") != + std::string::npos); +} + +TEST_F(PixTest, Validation_NonUniformResourceIndex_WaveOpsFlag) { + const char *source = R"x( +Texture2D textures[] : register(t0); +SamplerState samp : register(s0); + +cbuffer Constants : register(b0) +{ + uint index; +}; + +float4 main(float4 pos : SV_Position) : SV_Target +{ + return textures[index].Sample(samp, pos.xy); +})x"; + + // This index is dynamic and unmarked, so the pass instruments it; an + // index already marked NonUniformResourceIndex would be skipped. + // Instrumentation inserts WaveActiveAllEqual, which requires the WaveOps + // shader flag. + auto compiled = Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); + CComPtr dxil = FindModule(DFCC_ShaderDebugInfoDXIL, compiled); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::array Options = { + L"-opt-mod-passes", L"-dxil-dbg-value-to-dbg-declare", + L"-dxil-annotate-with-virtual-regs", + L"-hlsl-dxil-non-uniform-resource-index-instrumentation"}; + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + dxil, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + verifyInstrumentedModuleIsValid(pOptimizedModule, + "non-uniform resource index instrumentation"); + + VERIFY_ARE_NOT_EQUAL( + std::string::npos, + Disassemble(pOptimizedModule).find("dx.op.waveActiveAllEqual")); +} From 79770f0568b2b0ebbade52da34bfc0851f1d7312 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 1 Oct 2026 14:57:14 -0700 Subject: [PATCH 35/39] [PIX] Record only real resource accesses, including samplers Shader access tracking no longer records annotateHandle or barrierByMemoryHandle as accesses, still looks through annotated handles to the resource, and matches library handles against samplers so sampler accesses are recorded. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilShaderAccessTracking.cpp | 20 +++++ .../AccessTrackingBarrierIsNotAnAccess.hlsl | 27 +++++++ ...rackingLibAnnotateHandleIsNotAnAccess.hlsl | 30 ++++++++ ...ngLibAnnotatedHandleReadStillRecorded.hlsl | 33 ++++++++ tools/clang/unittests/HLSL/PixTest.cpp | 76 ++++++++++++++++++- 5 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index ed1d1b26cc..0774befdf4 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -659,6 +659,17 @@ DxilResourceAndClass DxilShaderAccessTracking::DetermineAccessForHandleForLib( } } } + if (ret.registerType == RegisterType::Invalid) { + auto const &Samplers = DM.GetSamplers(); + for (auto &Sampler : Samplers) { + if (global == Sampler->GetGlobalSymbol()) { + binding = + hlsl::resource_helper::loadBindingFromResourceBase(Sampler.get()); + ret.registerType = RegisterType::Sampler; + break; + } + } + } if (ret.registerType != RegisterType::Invalid) { ret.accessStyle = AccessStyle::FromRootSig; ret.RegisterID = binding.rangeLowerBound; @@ -898,6 +909,15 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { // Special cases switch (opCode) { + case DXIL::OpCode::AnnotateHandle: + // annotateHandle attaches type information. It is not a resource + // access. GetResourceFromHandle still walks through it when a + // later access uses the annotated handle. + continue; + case DXIL::OpCode::BarrierByMemoryHandle: + // A barrier orders accesses to a resource. It is not itself an + // access. + continue; case DXIL::OpCode::GetDimensions: // readWrite = ShaderAccessFlags::DescriptorRead; // TODO: Support // GetDimensions diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl new file mode 100644 index 0000000000..73da834b3e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingBarrierIsNotAnAccess.hlsl @@ -0,0 +1,27 @@ +// RUN: %dxc -T cs_6_8 -E main -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=U0:0:2i0;.0;256;512. | %FileCheck %s + +// Barrier() on a resource handle orders accesses to that resource. It is +// not itself an access. +// +// The config puts the UAVs of space 0 at slot 0 onwards, so g_out is slot +// 0 and g_rw is slot 1. A slot is three dwords, so g_out's write dword is +// at byte 4 and g_rw's write dword is at byte 16. + +// g_rw is only barriered, never accessed, so nothing is recorded against +// it. +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +// The store to g_out is a genuine write and is recorded. +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 4, + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +RWByteAddressBuffer g_out : register(u0); +RWTexture2D g_rw : register(u1); + +[numthreads(1, 1, 1)] +void main(uint index : SV_GroupIndex) +{ + Barrier(g_rw, DEVICE_SCOPE); + g_out.Store(0, 1); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl new file mode 100644 index 0000000000..d81de35182 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotateHandleIsNotAnAccess.hlsl @@ -0,0 +1,30 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=S0:1:1i0;U0:2:1i0;.0;0;0. | %FileCheck %s + +// annotateHandle attaches type information to a handle. It is not a +// memory operation. g_untouched is only passed to GetDimensions, which +// this pass skips, so the annotation is that resource's only handle use. +// Nothing is recorded against it. +// +// The config puts the SRV of space 0 at slot 1 and the UAV of space 0 at +// slot 2. A slot is three dwords, so g_untouched's read dword is at byte +// 12 and its write dword at byte 16. g_output's write dword is at byte 28. + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 12, +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +// The store to g_output is a genuine access and is recorded: +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 28, + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 12, +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +Texture2D g_untouched : register(t0); +RWByteAddressBuffer g_output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + uint width, height; + g_untouched.GetDimensions(width, height); + g_output.Store(0, width + height); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl new file mode 100644 index 0000000000..b14588c0a4 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibAnnotatedHandleReadStillRecorded.hlsl @@ -0,0 +1,33 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=S0:1:1i0;U0:2:1i0;.256;512;1024. | %FileCheck %s + +// annotateHandle is not an access, but a genuine access that uses an +// annotated handle still records the resource class from that annotation. +// +// Offsets with this config (SRV space 0 at slot 1, UAV space 0 at slot 2, +// three dwords per slot, descriptor-heap records at byte 256): +// g_input read slot 1, read dword -> 12 +// g_input write slot 1, write dword -> 16 (must not appear) +// g_output write slot 2, write dword -> 28 +// heapTexture read descriptor 3 -> 292 +// +// A descriptor-heap record encodes shader kind in its top four bits and +// ResourceAccessStyle in the next four. RayGeneration is 7 and SRVRead is +// 5, so 0x75000000 == 1962934272. + +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 12, +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 292, i32 undef, i32 1962934272, +// CHECK: call void @dx.op.bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 28, +// CHECK-NOT: bufferStore.i32(i32 69, %dx.types.Handle {{.*}}, i32 16, + +ByteAddressBuffer g_input : register(t0); +RWByteAddressBuffer g_output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + Texture2D heapTexture = ResourceDescriptorHeap[3]; + uint value = g_input.Load(0); + value += asuint(heapTexture.Load(int3(0, 0, 0)).x); + g_output.Store(0, value); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index c99b249b0f..aefb1ed4be 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -123,6 +123,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(AccessTracking_ModificationReport_Read) TEST_METHOD(AccessTracking_ModificationReport_Write) TEST_METHOD(AccessTracking_ModificationReport_SM66) + TEST_METHOD(AccessTracking_SamplerAccessInLibrary) TEST_METHOD(PixStructAnnotation_Lib_DualRaygen) @@ -192,6 +193,7 @@ class PixTest : public ::testing::Test { TEST_METHOD(Validation_ControlInvalidPixMetadataIsRejected) TEST_METHOD(Validation_ControlBoilerplateOnlyFailureIsRejected) TEST_METHOD(Validation_NonUniformResourceIndex_WaveOpsFlag) + TEST_METHOD(Validation_ShaderAccessTracking_DynamicallyIndexedResource) dxc::DxCompilerDllLoader m_dllSupport; VersionSupportInfo m_ver; @@ -753,7 +755,8 @@ class PixTest : public ::testing::Test { const wchar_t *profile = L"as_6_5"); void ValidateAllocaWrite(std::vector const &allocaWrites, size_t index, const char *name); - PassOutput RunShaderAccessTrackingPass(IDxcBlob *blob); + PassOutput RunShaderAccessTrackingPass( + IDxcBlob *blob, const wchar_t *config = L"U0:0:10i0;U0:1:2i0;.0;0;0."); CComPtr RunDxilPIXAddTidToAmplificationShaderPayloadPass(IDxcBlob *blob); CComPtr RunDxilPIXMeshShaderOutputPass(IDxcBlob *blob); @@ -1038,14 +1041,17 @@ TEST_F(PixTest, CompileDebugDisasmPDB) { VERIFY_SUCCEEDED(pCompiler->Disassemble(pPdbBlob, &pDisasm)); } -PassOutput PixTest::RunShaderAccessTrackingPass(IDxcBlob *blob) { +PassOutput PixTest::RunShaderAccessTrackingPass(IDxcBlob *blob, + const wchar_t *config) { CComPtr pOptimizer; VERIFY_SUCCEEDED( m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); std::vector Options; Options.push_back(L"-opt-mod-passes"); - Options.push_back(L"-hlsl-dxil-pix-shader-access-instrumentation,config=U0:0:" - L"10i0;U0:1:2i0;.0;0;0."); + std::wstring passOption = + L"-hlsl-dxil-pix-shader-access-instrumentation,config="; + passOption += config; + Options.push_back(passOption.c_str()); CComPtr pOptimizedModule; CComPtr pText; @@ -1404,6 +1410,47 @@ float main() : SV_Target ValidateAccessTrackingMods(hlsl, true); } +std::vector Split(std::string str, char delimeter); + +static bool HasBufferStoreWithByteOffset(std::vector const &lines, + unsigned byteOffset) { + std::string needle = "i32 " + std::to_string(byteOffset); + for (auto const &line : lines) { + if (line.find("dx.op.bufferStore") != std::string::npos && + line.find(needle) != std::string::npos) { + return true; + } + } + return false; +} + +TEST_F(PixTest, AccessTracking_SamplerAccessInLibrary) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +Texture2D g_texture : register(t0); +SamplerState g_sampler : register(s2); +RWByteAddressBuffer g_output : register(u0); + +[shader("raygeneration")] +void RayGen() +{ + float4 value = g_texture.SampleLevel(g_sampler, float2(0, 0), 0); + g_output.Store(0, asuint(value.x)); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"lib_6_6", {L"-Od"}); + auto output = RunShaderAccessTrackingPass( + compiled, L"S0:0:4i0;M0:20:4i0;U0:40:4i0;.0;0;0."); + auto lines = Split(Disassemble(output.blob), '\n'); + VERIFY_IS_TRUE(HasBufferStoreWithByteOffset(lines, 264)); + verifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of a library sampler access"); +} + TEST_F(PixTest, AddToASGroupSharedPayload) { const char *hlsl = R"( @@ -4833,3 +4880,24 @@ float4 main(float4 pos : SV_Position) : SV_Target std::string::npos, Disassemble(pOptimizedModule).find("dx.op.waveActiveAllEqual")); } + +TEST_F(PixTest, Validation_ShaderAccessTracking_DynamicallyIndexedResource) { + const char *source = R"x( +Texture2D textures[8] : register(t0); +SamplerState samp : register(s0); + +cbuffer Constants : register(b0) +{ + uint index; +}; + +float4 main(float4 pos : SV_Position) : SV_Target +{ + return textures[index].Sample(samp, pos.xy); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = RunShaderAccessTrackingPass(compiled); + verifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of a dynamically indexed resource"); +} From d7808b1200b03059a42b0d61d0da136494115aa6 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 1 Oct 2026 14:58:36 -0700 Subject: [PATCH 36/39] [PIX] Fix dynamic bind-point identity and range bounds Make RSRegisterIdentifier a strict weak ordering, report the register a binding occupies rather than its resource-list position, and fix the off-by-one range check for constant indices. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilShaderAccessTracking.cpp | 12 ++- tools/clang/unittests/HLSL/PixTest.cpp | 83 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index 0774befdf4..62f54cfec2 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -174,8 +174,9 @@ struct RSRegisterIdentifier { unsigned Index; bool operator<(const RSRegisterIdentifier &o) const { - return static_cast(Type) < static_cast(o.Type) && - Space < o.Space && Index < o.Index; + return static_cast(Type) < static_cast(o.Type) || + (Type == o.Type && + (Space < o.Space || (Space == o.Space && Index < o.Index))); } }; @@ -428,7 +429,8 @@ bool DxilShaderAccessTracking::EmitResourceAccess(DxilModule &DM, if (isa(res.index) && res.indexDynamicOffset == nullptr) { unsigned index = cast(res.index)->getLimitedValue(); - if (index > slot->second.numSlots) { + // Index is 0-based, so numSlots is the first out-of-range value. + if (index >= slot->second.numSlots) { // out-of-range accesses are written to slot zero: slotIndex = HlslOP->GetU32Const(0); } else { @@ -746,7 +748,8 @@ DxilShaderAccessTracking::GetResourceFromHandle(Value *resHandle, ret.index = createHandle.get_index(); ret.registerType = registerType; ret.accessStyle = AccessStyle::FromRootSig; - ret.RegisterID = resource->GetID(); + // RegisterID is the binding lower bound, not the resource-list ID. + ret.RegisterID = resource->GetLowerBound(); ret.RegisterSpace = resource->GetSpaceID(); } } @@ -768,6 +771,7 @@ DxilShaderAccessTracking::GetResourceFromHandle(Value *resHandle, ret.index = createHandleFromBinding.get_index(); ret.registerType = RegisterTypeFromResourceClass( static_cast(binding.resourceClass)); + ret.RegisterID = binding.rangeLowerBound; ret.RegisterSpace = binding.spaceID; } else if (hlsl::OP::IsDxilOpFuncCallInst( handleCreation, hlsl::OP::OpCode::CreateHandleFromHeap)) { diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index aefb1ed4be..ec84f442fd 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -123,6 +123,9 @@ class PixTest : public ::testing::Test { TEST_METHOD(AccessTracking_ModificationReport_Read) TEST_METHOD(AccessTracking_ModificationReport_Write) TEST_METHOD(AccessTracking_ModificationReport_SM66) + TEST_METHOD(AccessTracking_MultipleDynamicRangesSameTypeAndSpace) + TEST_METHOD(AccessTracking_DynamicRangeRegisterIndex_SM66) + TEST_METHOD(AccessTracking_ConstantIndexAtRangeLimit) TEST_METHOD(AccessTracking_SamplerAccessInLibrary) TEST_METHOD(PixStructAnnotation_Lib_DualRaygen) @@ -1412,6 +1415,15 @@ float main() : SV_Target std::vector Split(std::string str, char delimeter); +static std::string JoinLines(std::vector const &lines) { + std::string joined; + for (auto const &line : lines) { + joined += line; + joined += '\n'; + } + return joined; +} + static bool HasBufferStoreWithByteOffset(std::vector const &lines, unsigned byteOffset) { std::string needle = "i32 " + std::to_string(byteOffset); @@ -1424,6 +1436,77 @@ static bool HasBufferStoreWithByteOffset(std::vector const &lines, return false; } +TEST_F(PixTest, AccessTracking_MultipleDynamicRangesSameTypeAndSpace) { + const char *hlsl = R"( +ByteAddressBuffer g_indices : register(t0); +RWByteAddressBuffer g_firstRange[2] : register(u4); +RWByteAddressBuffer g_secondRange[2] : register(u6); + +[numthreads(1, 1, 1)] +void CSMain() +{ + uint index = g_indices.Load(0); + g_firstRange[index].Store(0, 1); + g_secondRange[index].Store(0, 2); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"cs_6_0", {L"-Od"}, L"CSMain"); + auto output = + RunShaderAccessTrackingPass(compiled, L"S0:0:2i0;U0:0:10i0;.0;0;0."); + auto text = JoinLines(output.lines); + VERIFY_IS_TRUE(text.find("U0:4;") != std::string::npos); + VERIFY_IS_TRUE(text.find("U0:6;") != std::string::npos); + verifyInstrumentedModuleIsValid(output.blob, + "shader access tracking of two dynamic UAV " + "ranges in the same register space"); +} + +TEST_F(PixTest, AccessTracking_DynamicRangeRegisterIndex_SM66) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +RWByteAddressBuffer g_buffers[] : register(u5); + +[numthreads(1, 1, 1)] +void CSMain(uint3 dispatchThreadId : SV_DispatchThreadID) +{ + g_buffers[dispatchThreadId.x].Store(0, 1); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"cs_6_6", {L"-Od"}, L"CSMain"); + auto output = RunShaderAccessTrackingPass(compiled, L"U0:0:10i0;.0;0;0."); + auto text = JoinLines(output.lines); + VERIFY_IS_TRUE(text.find("U0:5;") != std::string::npos); + VERIFY_IS_TRUE(text.find("U0:0;") == std::string::npos); + verifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of an SM 6.6 dynamic UAV range"); +} + +TEST_F(PixTest, AccessTracking_ConstantIndexAtRangeLimit) { + const char *hlsl = R"( +RWByteAddressBuffer g_buffers[] : register(u0); + +[numthreads(1, 1, 1)] +void CSMain() +{ + g_buffers[1].Store(0, 1); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"cs_6_0", {L"-Od"}, L"CSMain"); + auto output = RunShaderAccessTrackingPass(compiled, L"U0:0:1i0;.0;0;0."); + auto lines = Split(Disassemble(output.blob), '\n'); + VERIFY_IS_TRUE(HasBufferStoreWithByteOffset(lines, 4)); + VERIFY_IS_TRUE(!HasBufferStoreWithByteOffset(lines, 16)); + verifyInstrumentedModuleIsValid( + output.blob, + "shader access tracking of a constant index at the range limit"); +} + TEST_F(PixTest, AccessTracking_SamplerAccessInLibrary) { if (m_ver.SkipDxilVersion(1, 6)) { return; From 7acdd2bbcc3fbfb4a1c12ee72377607f104389aa Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 1 Oct 2026 14:59:49 -0700 Subject: [PATCH 37/39] [PIX] Attribute library accesses to reaching entry points Attribute accesses in library helper functions to the pipeline stage of the entry points that reach them instead of the Library kind, and keep instruction numbers from overflowing the 24-bit record field. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilShaderAccessTracking.cpp | 106 +++++++++++++++--- .../AccessTrackingLibHelperShaderKind.hlsl | 31 +++++ tools/clang/unittests/HLSL/PixTest.cpp | 82 ++++++++++++++ 3 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl diff --git a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp index 62f54cfec2..c48533f85d 100644 --- a/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp +++ b/lib/DxilPIXPasses/DxilShaderAccessTracking.cpp @@ -28,6 +28,8 @@ #include "llvm/Transforms/Utils/Local.h" #include +#include +#include #include "PixPassHelpers.h" @@ -546,11 +548,16 @@ bool DxilShaderAccessTracking::EmitResourceAccess(DxilModule &DM, Builder.CreateMul(ZeroIfOutOfBounds, EncodedFlags); uint32_t InstructionNumber = 0; (void)pix_dxil::PixDxilInstNum::FromInst(instruction, &InstructionNumber); - auto const *shaderModel = DM.GetShaderModel(); - auto shaderKind = shaderModel->GetKind(); - uint32_t EncodedInstructionNumber = InstructionNumber | - InstructionOrdinalndicator | - EncodeShaderModel(shaderKind); + auto *EncodedShaderKindConstant = cast( + m_FunctionToEncodedAccess.at(Builder.GetInsertBlock()->getParent()) + .at(ResourceAccessStyle::None)); + uint32_t EncodedShaderKind = EncodedShaderKindConstant->getLimitedValue(); + // The ordinal occupies the low 24 bits. Mask it so it does not overlap + // the indicator or the shader kind. + constexpr uint32_t InstructionOrdinalMask = 0x00FF'FFFF; + uint32_t EncodedInstructionNumber = + (InstructionNumber & InstructionOrdinalMask) | + InstructionOrdinalndicator | EncodedShaderKind; auto *MultipliedOutOfBoundsValue = Builder.CreateMul( OneIfOutOfBounds, HlslOP->GetU32Const(EncodedInstructionNumber)); auto *CombinedFlagOrInstructionValue = @@ -810,6 +817,69 @@ DxilShaderAccessTracking::GetResourceFromHandle(Value *resHandle, return ret; } +// Map each function to the shader kind of the entry point that reaches it. +// A library helper has no DxilFunctionProps, so the module kind is Library, +// which PIX cannot attribute to a pipeline stage. If more than one entry +// kind reaches the same helper, keep the module kind. Entry points keep +// their own kind. +static std::map +ResolveShaderKindByReachingEntryPoint(DxilModule &DM) { + std::map functionToShaderKind; + + const DXIL::ShaderKind ambiguousShaderKind = DM.GetShaderModel()->GetKind(); + auto entryPoints = DM.GetExportedFunctions(); + + for (llvm::Function *entryPoint : entryPoints) { + if (entryPoint == nullptr || entryPoint->isDeclaration()) { + continue; + } + + const DXIL::ShaderKind entryPointShaderKind = + PIXPassHelpers::GetFunctionShaderKind(DM, entryPoint); + + std::vector pending{entryPoint}; + std::set visited; + while (!pending.empty()) { + llvm::Function *reached = pending.back(); + pending.pop_back(); + if (!visited.insert(reached).second) { + continue; + } + + auto emplaced = + functionToShaderKind.emplace(reached, entryPointShaderKind); + if (!emplaced.second && emplaced.first->second != entryPointShaderKind) { + emplaced.first->second = ambiguousShaderKind; + } + + for (llvm::BasicBlock &block : reached->getBasicBlockList()) { + for (llvm::Instruction &instruction : block.getInstList()) { + auto *call = llvm::dyn_cast(&instruction); + if (call == nullptr) { + continue; + } + llvm::Function *callee = call->getCalledFunction(); + if (callee == nullptr || callee->isDeclaration() || + callee->isIntrinsic() || hlsl::OP::IsDxilOpFunc(callee)) { + continue; + } + pending.push_back(callee); + } + } + } + } + + for (llvm::Function *entryPoint : entryPoints) { + if (entryPoint == nullptr || entryPoint->isDeclaration()) { + continue; + } + functionToShaderKind[entryPoint] = + PIXPassHelpers::GetFunctionShaderKind(DM, entryPoint); + } + + return functionToShaderKind; +} + bool DxilShaderAccessTracking::runOnModule(Module &M) { // This pass adds instrumentation for shader access to resources @@ -840,6 +910,8 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { auto instrumentableFunctions = PIXPassHelpers::GetAllInstrumentableFunctions(DM); + auto functionToShaderKind = ResolveShaderKindByReachingEntryPoint(DM); + if (DM.m_ShaderFlags.GetForceEarlyDepthStencil()) { if (OSOverride != nullptr) { formatted_raw_ostream FOS(*OSOverride); @@ -851,17 +923,11 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { PIXPassHelpers::CreateGlobalUAVResource(DM, 0u, "PIX_ShaderAccessUAV"); for (auto *F : instrumentableFunctions) { - DXIL::ShaderKind shaderKind = DXIL::ShaderKind::Invalid; - if (!DM.HasDxilFunctionProps(F)) { - auto ShaderModel = DM.GetShaderModel(); - shaderKind = ShaderModel->GetKind(); - if (shaderKind == DXIL::ShaderKind::Library) { - continue; - } - } else { - hlsl::DxilFunctionProps const &props = DM.GetDxilFunctionProps(F); - shaderKind = props.shaderKind; - } + auto reachedFrom = functionToShaderKind.find(F); + DXIL::ShaderKind shaderKind = + reachedFrom != functionToShaderKind.end() + ? reachedFrom->second + : PIXPassHelpers::GetFunctionShaderKind(DM, F); IRBuilder<> Builder(F->getEntryBlock().getFirstInsertionPt()); @@ -959,9 +1025,13 @@ bool DxilShaderAccessTracking::runOnModule(Module &M) { } for (unsigned iParam : handleParams) { + auto uavHandle = + m_FunctionToUAVHandle.find(CallerParent->getParent()); + if (uavHandle == m_FunctionToUAVHandle.end()) + continue; + // Don't instrument the accesses to the UAV that we just added - if (Call->getArgOperand(iParam) == - m_FunctionToUAVHandle[CallerParent->getParent()]) + if (Call->getArgOperand(iParam) == uavHandle->second) continue; auto res = GetResourceFromHandle(Call->getArgOperand(iParam), DM); if (res.accessStyle == AccessStyle::None) { diff --git a/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl new file mode 100644 index 0000000000..a998d42218 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/AccessTrackingLibHelperShaderKind.hlsl @@ -0,0 +1,31 @@ +// RUN: %dxc -T lib_6_6 -Od %s | %opt -S -hlsl-dxil-pix-shader-access-instrumentation,config=.256;512;1024. | %FileCheck %s + +// A descriptor-heap access record carries the shader kind of the entry +// point that reached the access. HeapHelper is [noinline] so the access +// stays in the helper. The descriptor index is a parameter so the helper +// is not folded away. +// +// The kind occupies bits 31:28. An out-of-bounds record sets the +// instruction-ordinal indicator (bit 27). RayGeneration is 7 and UAVWrite +// is 3, so the in-bounds flags are 0x73000000 == 1929379840 and the +// out-of-bounds value is 0x78000000 == 2013265920. Under the module kind, +// Library (6), those values would be 0x63000000 and 0x68000000. + +// CHECK: define void {{.*}}HeapHelper +// CHECK-NOT: 1660944384 +// CHECK: mul i32 {{.*}}, 1929379840 +// CHECK-NOT: 1744830464 +// CHECK: mul i32 {{.*}}, 2013265920 + +[noinline] +export void HeapHelper(uint descriptorIndex) +{ + RWByteAddressBuffer heapBuffer = ResourceDescriptorHeap[descriptorIndex]; + heapBuffer.Store(0, 1); +} + +[shader("raygeneration")] +void RayGen() +{ + HeapHelper(1); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index ec84f442fd..e995a9f232 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -127,6 +127,8 @@ class PixTest : public ::testing::Test { TEST_METHOD(AccessTracking_DynamicRangeRegisterIndex_SM66) TEST_METHOD(AccessTracking_ConstantIndexAtRangeLimit) TEST_METHOD(AccessTracking_SamplerAccessInLibrary) + TEST_METHOD(AccessTracking_OobBindlessUsesFunctionShaderKind) + TEST_METHOD(AccessTracking_LibraryNonEntryFunction) TEST_METHOD(PixStructAnnotation_Lib_DualRaygen) @@ -1436,6 +1438,28 @@ static bool HasBufferStoreWithByteOffset(std::vector const &lines, return false; } +static bool +HasBufferStoreValueMatchingMask(std::vector const &lines, + uint32_t mask, uint32_t maskedValue) { + for (auto const &line : lines) { + if (line.find("dx.op.bufferStore") == std::string::npos) { + continue; + } + + size_t position = 0; + while ((position = line.find("i32 ", position)) != std::string::npos) { + position += 4; + char *end = nullptr; + uint32_t value = + static_cast(strtoul(line.c_str() + position, &end, 10)); + if (end != line.c_str() + position && (value & mask) == maskedValue) { + return true; + } + } + } + return false; +} + TEST_F(PixTest, AccessTracking_MultipleDynamicRangesSameTypeAndSpace) { const char *hlsl = R"( ByteAddressBuffer g_indices : register(t0); @@ -1534,6 +1558,64 @@ void RayGen() output.blob, "shader access tracking of a library sampler access"); } +TEST_F(PixTest, AccessTracking_OobBindlessUsesFunctionShaderKind) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +[shader("raygeneration")] +void RayGen() +{ + RWByteAddressBuffer output = ResourceDescriptorHeap[1]; + output.Store(0, 1); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"lib_6_6", {L"-Od"}); + auto output = RunShaderAccessTrackingPass(compiled, L".0;0;0."); + auto lines = Split(Disassemble(output.blob), '\n'); + VERIFY_IS_TRUE( + HasBufferStoreValueMatchingMask(lines, 0xF8000000, 0x78000000)); + VERIFY_IS_TRUE( + !HasBufferStoreValueMatchingMask(lines, 0xF8000000, 0x68000000)); + verifyInstrumentedModuleIsValid( + output.blob, + "shader access tracking of an out-of-bounds bindless access"); +} + +TEST_F(PixTest, AccessTracking_LibraryNonEntryFunction) { + if (m_ver.SkipDxilVersion(1, 6)) { + return; + } + + const char *hlsl = R"( +Texture2D g_texture : register(t0); +RWByteAddressBuffer g_output : register(u0); + +export float4 Helper(uint index) +{ + float4 value = g_texture.Load(int3(index, 0, 0)); + g_output.Store(0, asuint(value.x)); + return value; +} + +[shader("raygeneration")] +void RayGen() +{ + Helper(0); +} +)"; + + auto compiled = Compile(m_dllSupport, hlsl, L"lib_6_6", {L"-Od"}); + auto output = + RunShaderAccessTrackingPass(compiled, L"S0:0:4i0;U0:4:4i0;.0;0;0."); + auto text = JoinLines(output.lines); + VERIFY_IS_TRUE(text.find("NotModified") == std::string::npos); + verifyInstrumentedModuleIsValid( + output.blob, "shader access tracking of a library helper function"); +} + TEST_F(PixTest, AddToASGroupSharedPayload) { const char *hlsl = R"( From ddc1237c099884aab67053dcb22f339f5697d37c Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 1 Oct 2026 15:01:06 -0700 Subject: [PATCH 38/39] [PIX] Fix render-state visualization passes DxilOutputColorBecomesConstant handles all storeOutput overloads (including 16-bit), gives its constant buffer a layout the validator accepts, and DxilReduceMSAAToSingleSample looks through annotateHandle and accepts 16-bit texture loads. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilOutputColorBecomesConstant.cpp | 195 +++++++++++------- .../DxilReduceMSAAToSingleSample.cpp | 95 +++++---- .../HLSLFileCheck/pix/constantcolorhalf.hlsl | 19 ++ .../pix/constantcolorhalfFromCB.hlsl | 36 ++++ .../pix/constantcolorhalfMRT.hlsl | 29 +++ .../pix/constantcolorhalfMRTOnRTV1.hlsl | 33 +++ .../HLSLFileCheck/pix/constantcolorint16.hlsl | 18 ++ .../pix/constantcolorminprecision.hlsl | 18 ++ .../pix/constantcolorminprecisionFromCB.hlsl | 37 ++++ .../pix/constantcolorminprecisionMRT.hlsl | 29 +++ .../pix/constantcolorminprecisionint.hlsl | 18 ++ tools/clang/unittests/HLSL/PixTest.cpp | 118 +++++++++++ 12 files changed, 532 insertions(+), 113 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl diff --git a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp index 7ecb712ea2..036858e55e 100644 --- a/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp +++ b/lib/DxilPIXPasses/DxilOutputColorBecomesConstant.cpp @@ -12,6 +12,7 @@ #include "dxc/DXIL/DxilModule.h" #include "dxc/DXIL/DxilOperations.h" +#include "dxc/DXIL/DxilTypeSystem.h" #include "dxc/DxilPIXPasses/DxilPIXPasses.h" #include "dxc/HLSL/DxilGenerationPass.h" #include "dxc/HLSL/DxilSpanAllocator.h" @@ -108,52 +109,78 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { const hlsl::DxilSignature &OutputSignature = DM.GetOutputSignature(); - Function *FloatOutputFunction = - HlslOP->GetOpFunc(DXIL::OpCode::StoreOutput, Type::getFloatTy(Ctx)); - Function *IntOutputFunction = - HlslOP->GetOpFunc(DXIL::OpCode::StoreOutput, Type::getInt32Ty(Ctx)); + // dx.op.storeOutput has four legal overloads: f16, f32, i16 and i32. + // A min16float or min16int SV_Target lowers through the f16 or i16 form, + // as does a native half or int16_t target under -enable-16bit-types. + const std::array OverloadTypes{ + Type::getHalfTy(Ctx), Type::getFloatTy(Ctx), Type::getInt16Ty(Ctx), + Type::getInt32Ty(Ctx)}; - bool hasFloatOutputs = false; - bool hasIntOutputs = false; + std::array OutputFunctions{}; + size_t ActiveOverload = OverloadTypes.size(); - visitOutputInstructionCallers( - FloatOutputFunction, OutputSignature, HlslOP, - [&hasFloatOutputs](CallInst *) { hasFloatOutputs = true; }); + for (size_t OverloadIndex = 0; OverloadIndex < OverloadTypes.size(); + ++OverloadIndex) { + OutputFunctions[OverloadIndex] = HlslOP->GetOpFunc( + DXIL::OpCode::StoreOutput, OverloadTypes[OverloadIndex]); - visitOutputInstructionCallers( - IntOutputFunction, OutputSignature, HlslOP, - [&hasIntOutputs](CallInst *) { hasIntOutputs = true; }); + bool HasTargetZeroStores = false; + visitOutputInstructionCallers( + OutputFunctions[OverloadIndex], OutputSignature, HlslOP, + [&HasTargetZeroStores](CallInst *) { HasTargetZeroStores = true; }); + + if (HasTargetZeroStores) { + // visitOutputInstructionCallers filters on SemanticKind::Target with + // GetSemanticStartIndex() == 0, so at most one overload writes + // SV_Target0. + DXASSERT(ActiveOverload == OverloadTypes.size(), + "Only one storeOutput overload can write SV_Target0"); + ActiveOverload = OverloadIndex; + } + } - if (!hasFloatOutputs && !hasIntOutputs) { - bool Modified = PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); - Modified |= PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); - return Modified; + // GetOpFunc materialises each overload declaration on demand. Any + // overload with no callers must be erased before the pass returns; the + // validator rejects a module carrying an unused dx.op declaration. + struct EraseUnusedOutputFunctionsOnExit { + hlsl::DxilModule &DM; + std::array &OutputFunctions; + ~EraseUnusedOutputFunctionsOnExit() { + for (Function *OutputFunction : OutputFunctions) { + PIXPassHelpers::eraseIfUnused(DM, OutputFunction); + } + } + } EraseUnusedOutputFunctions{DM, OutputFunctions}; + + if (ActiveOverload == OverloadTypes.size()) { + return false; } - // Otherwise, we assume the shader outputs only one or the other (because the - // 0th RTV can't have a mixed type) - DXASSERT(!hasFloatOutputs || !hasIntOutputs, - "Only one or the other type of output: float or int"); + // Replacement values must match the store's own overload type. + llvm::Type *const OutputValueType = OverloadTypes[ActiveOverload]; + const bool IsFloatOutput = OutputValueType->isFloatingPointTy(); std::array ReplacementColors; switch (Mode) { case FromLiteralConstant: { - if (hasFloatOutputs) { - ReplacementColors[0] = HlslOP->GetFloatConst(Red); - ReplacementColors[1] = HlslOP->GetFloatConst(Green); - ReplacementColors[2] = HlslOP->GetFloatConst(Blue); - ReplacementColors[3] = HlslOP->GetFloatConst(Alpha); - } - if (hasIntOutputs) { - ReplacementColors[0] = HlslOP->GetI32Const(static_cast(Red)); - ReplacementColors[1] = HlslOP->GetI32Const(static_cast(Green)); - ReplacementColors[2] = HlslOP->GetI32Const(static_cast(Blue)); - ReplacementColors[3] = HlslOP->GetI32Const(static_cast(Alpha)); + const std::array Channels{Red, Green, Blue, Alpha}; + for (size_t ChannelIndex = 0; ChannelIndex < Channels.size(); + ++ChannelIndex) { + ReplacementColors[ChannelIndex] = + IsFloatOutput + ? ConstantFP::get(OutputValueType, Channels[ChannelIndex]) + : ConstantInt::get(OutputValueType, + static_cast(static_cast( + Channels[ChannelIndex])), + /*isSigned*/ true); } } break; case FromConstantBuffer: { + // A float4 constant buffer row is 16 bytes wide. + constexpr unsigned int ConstantColorCBufferSizeInBytes = 4 * sizeof(float); + // Setup a constant buffer with a single float4 in it: SmallVector Elements{ Type::getFloatTy(Ctx), Type::getFloatTy(Ctx), Type::getFloatTy(Ctx), @@ -162,13 +189,31 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { llvm::StructType::create(Elements, "PIX_ConstantColorCB_Type"); std::unique_ptr pCBuf = llvm::make_unique(); pCBuf->SetGlobalName("PIX_ConstantColorCBName"); - pCBuf->SetGlobalSymbol(UndefValue::get(CBStructTy)); + // The global symbol and HLSL type must be pointers to the struct so + // ValidateCBuffer can reach the annotation. + pCBuf->SetGlobalSymbol(UndefValue::get(CBStructTy->getPointerTo())); + pCBuf->SetHLSLType(CBStructTy->getPointerTo()); pCBuf->SetID(static_cast(DM.GetCBuffers().size())); pCBuf->SetSpaceID( (unsigned int)-2); // This is the reserved-for-tools register space pCBuf->SetLowerBound(0); pCBuf->SetRangeSize(1); - pCBuf->SetSize(4); + pCBuf->SetSize(ConstantColorCBufferSizeInBytes); + + auto *StructAnnotation = DM.GetTypeSystem().GetStructAnnotation(CBStructTy); + if (StructAnnotation == nullptr) { + StructAnnotation = DM.GetTypeSystem().AddStructAnnotation(CBStructTy); + StructAnnotation->SetCBufferSize(ConstantColorCBufferSizeInBytes); + static const char *const ComponentNames[] = {"r", "g", "b", "a"}; + for (unsigned int ComponentIndex = 0; ComponentIndex < 4; + ++ComponentIndex) { + auto &FieldAnnotation = + StructAnnotation->GetFieldAnnotation(ComponentIndex); + FieldAnnotation.SetCBufferOffset(ComponentIndex * sizeof(float)); + FieldAnnotation.SetCompType(hlsl::DXIL::ComponentType::F32); + FieldAnnotation.SetFieldName(ComponentNames[ComponentIndex]); + } + } Instruction *entryPointInstruction = &*(PIXPassHelpers::GetEntryFunction(DM)->begin()->begin()); @@ -188,9 +233,12 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { #define PIX_CONSTANT_VALUE "PIX_Constant_Color_Value" // Insert the Buffer load instruction: - Function *CBLoad = HlslOP->GetOpFunc( - OP::OpCode::CBufferLoadLegacy, - hasFloatOutputs ? Type::getFloatTy(Ctx) : Type::getInt32Ty(Ctx)); + // The tools constant buffer is always four 32-bit components; PIX + // uploads that layout. + llvm::Type *const CBufferComponentType = + IsFloatOutput ? Type::getFloatTy(Ctx) : Type::getInt32Ty(Ctx); + Function *CBLoad = + HlslOP->GetOpFunc(OP::OpCode::CBufferLoadLegacy, CBufferComponentType); Constant *OpArg = HlslOP->GetU32Const((unsigned)OP::OpCode::CBufferLoadLegacy); Value *ResourceHandle = callCreateHandle; @@ -207,54 +255,47 @@ bool DxilOutputColorBecomesConstant::runOnModule(Module &M) { Builder.CreateExtractValue(loadLegacy, 2, PIX_CONSTANT_VALUE "2"); ReplacementColors[3] = Builder.CreateExtractValue(loadLegacy, 3, PIX_CONSTANT_VALUE "3"); + + // Narrow the loaded components to a 16-bit output overload. + if (OutputValueType != CBufferComponentType) { + static const char *const NarrowedNames[] = { + PIX_CONSTANT_VALUE "Narrowed0", PIX_CONSTANT_VALUE "Narrowed1", + PIX_CONSTANT_VALUE "Narrowed2", PIX_CONSTANT_VALUE "Narrowed3"}; + for (size_t ChannelIndex = 0; ChannelIndex < ReplacementColors.size(); + ++ChannelIndex) { + ReplacementColors[ChannelIndex] = + IsFloatOutput + ? Builder.CreateFPTrunc(ReplacementColors[ChannelIndex], + OutputValueType, + NarrowedNames[ChannelIndex]) + : Builder.CreateTrunc(ReplacementColors[ChannelIndex], + OutputValueType, + NarrowedNames[ChannelIndex]); + } + } } break; default: assert(false); - return 0; + return false; } bool Modified = false; - // The StoreOutput function can store either a float or an integer, depending - // on the intended output render-target resource view. - if (hasFloatOutputs) { - visitOutputInstructionCallers( - FloatOutputFunction, OutputSignature, HlslOP, - [&ReplacementColors, &Modified](CallInst *CallInstruction) { - Modified = true; - // The output column is the channel (red, green, blue or alpha) within - // the output pixel - Value *OutputColumnOperand = CallInstruction->getOperand( - hlsl::DXIL::OperandIndex::kStoreOutputColOpIdx); - ConstantInt *OutputColumnConstant = - cast(OutputColumnOperand); - APInt OutputColumn = OutputColumnConstant->getValue(); - CallInstruction->setOperand( - hlsl::DXIL::OperandIndex::kStoreOutputValOpIdx, - ReplacementColors[*OutputColumn.getRawData()]); - }); - } - - if (hasIntOutputs) { - visitOutputInstructionCallers( - IntOutputFunction, OutputSignature, HlslOP, - [&ReplacementColors, &Modified](CallInst *CallInstruction) { - Modified = true; - // The output column is the channel (red, green, blue or alpha) within - // the output pixel - Value *OutputColumnOperand = CallInstruction->getOperand( - hlsl::DXIL::OperandIndex::kStoreOutputColOpIdx); - ConstantInt *OutputColumnConstant = - cast(OutputColumnOperand); - APInt OutputColumn = OutputColumnConstant->getValue(); - CallInstruction->setOperand( - hlsl::DXIL::OperandIndex::kStoreOutputValOpIdx, - ReplacementColors[*OutputColumn.getRawData()]); - }); - } - - Modified |= PIXPassHelpers::eraseIfUnused(DM, FloatOutputFunction); - Modified |= PIXPassHelpers::eraseIfUnused(DM, IntOutputFunction); + visitOutputInstructionCallers( + OutputFunctions[ActiveOverload], OutputSignature, HlslOP, + [&ReplacementColors, &Modified](CallInst *CallInstruction) { + Modified = true; + // The output column is the channel (red, green, blue or alpha) within + // the output pixel + Value *OutputColumnOperand = CallInstruction->getOperand( + hlsl::DXIL::OperandIndex::kStoreOutputColOpIdx); + ConstantInt *OutputColumnConstant = + cast(OutputColumnOperand); + APInt OutputColumn = OutputColumnConstant->getValue(); + CallInstruction->setOperand( + hlsl::DXIL::OperandIndex::kStoreOutputValOpIdx, + ReplacementColors[*OutputColumn.getRawData()]); + }); return Modified; } diff --git a/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp b/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp index 01f06605a5..9cb685c647 100644 --- a/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp +++ b/lib/DxilPIXPasses/DxilReduceMSAAToSingleSample.cpp @@ -13,6 +13,7 @@ #include "dxc/DXIL/DxilInstructions.h" #include "dxc/DXIL/DxilModule.h" +#include "dxc/DXIL/DxilResourceProperties.h" #include "dxc/DxilPIXPasses/DxilPIXPasses.h" #include "dxc/HLSL/DxilGenerationPass.h" @@ -34,50 +35,72 @@ class DxilReduceMSAAToSingleSample : public ModulePass { bool runOnModule(Module &M) override; }; -bool DxilReduceMSAAToSingleSample::runOnModule(Module &M) { - DxilModule &DM = M.GetOrCreateDxilModule(); +static bool IsMultisampledSRVHandle(Value *TextureHandle, DxilModule &DM) { + auto *TextureHandleInst = dyn_cast(TextureHandle); + if (!TextureHandleInst) + return false; + + if (OP::IsDxilOpFuncCallInst(TextureHandleInst, OP::OpCode::CreateHandle)) { + DxilInst_CreateHandle CreateHandle(TextureHandleInst); + if (!isa(CreateHandle.get_rangeId())) + return false; + + if (static_cast( + CreateHandle.get_resourceClass_val()) != DXIL::ResourceClass::SRV) + return false; + + unsigned RangeId = + cast(CreateHandle.get_rangeId())->getLimitedValue(); + auto Resource = DM.GetSRV(RangeId); + return Resource.GetKind() == DXIL::ResourceKind::Texture2DMS || + Resource.GetKind() == DXIL::ResourceKind::Texture2DMSArray; + } - LLVMContext &Ctx = M.getContext(); - OP *HlslOP = DM.GetOP(); + // SM 6.6 handles carry the resource kind in the annotateHandle + // properties operand. + if (OP::IsDxilOpFuncCallInst(TextureHandleInst, OP::OpCode::AnnotateHandle)) { + DxilInst_AnnotateHandle AnnotateHandle(TextureHandleInst); + DxilResourceProperties ResourceProperties = + resource_helper::loadPropsFromAnnotateHandle(AnnotateHandle, + *DM.GetShaderModel()); + return ResourceProperties.getResourceClass() == DXIL::ResourceClass::SRV && + (ResourceProperties.getResourceKind() == + DXIL::ResourceKind::Texture2DMS || + ResourceProperties.getResourceKind() == + DXIL::ResourceKind::Texture2DMSArray); + } - // FP16 type doesn't have its own identity, and is covered by float type... + return false; +} - auto TextureLoadOverloads = std::vector{ - Type::getFloatTy(Ctx), Type::getInt16Ty(Ctx), Type::getInt32Ty(Ctx)}; +bool DxilReduceMSAAToSingleSample::runOnModule(Module &M) { + DxilModule &DM = M.GetOrCreateDxilModule(); + OP *HlslOP = DM.GetOP(); bool Modified = false; - for (const auto &Overload : TextureLoadOverloads) { + // Iterate every materialised TextureLoad overload; the 16-bit form + // lowers Texture2DMS.Load. + for (const auto &TextureLoadOverload : + HlslOP->GetOpFuncList(DXIL::OpCode::TextureLoad)) { + Function *TexLoadFunction = TextureLoadOverload.second; + if (!TexLoadFunction) + continue; - Function *TexLoadFunction = - HlslOP->GetOpFunc(DXIL::OpCode::TextureLoad, Overload); - auto TexLoadFunctionUses = TexLoadFunction->uses(); - - for (auto FI = TexLoadFunctionUses.begin(); - FI != TexLoadFunctionUses.end();) { + for (auto FI = TexLoadFunction->use_begin(); + FI != TexLoadFunction->use_end();) { auto &FunctionUse = *FI++; - auto FunctionUser = FunctionUse.getUser(); - auto instruction = cast(FunctionUser); - DxilInst_TextureLoad LoadInstruction(instruction); - auto TextureHandle = LoadInstruction.get_srv(); - auto TextureHandleInst = cast(TextureHandle); - DxilInst_CreateHandle createHandle(TextureHandleInst); - // Dynamic rangeId is not supported - if (isa(createHandle.get_rangeId())) { - unsigned rangeId = - cast(createHandle.get_rangeId())->getLimitedValue(); - if (static_cast( - createHandle.get_resourceClass_val()) == - DXIL::ResourceClass::SRV) { - auto Resource = DM.GetSRV(rangeId); - if (Resource.GetKind() == DXIL::ResourceKind::Texture2DMS || - Resource.GetKind() == DXIL::ResourceKind::Texture2DMSArray) { - // "2" is the mip-level/sample-index operand index: - // https://github.com/Microsoft/DirectXShaderCompiler/blob/master/docs/DXIL.rst#textureload - instruction->setOperand(2, HlslOP->GetI32Const(0)); - Modified = true; - } - } + auto *InstructionUser = dyn_cast(FunctionUse.getUser()); + if (!InstructionUser) + continue; + + DxilInst_TextureLoad LoadInstruction(InstructionUser); + if (!LoadInstruction) + continue; + + if (IsMultisampledSRVHandle(LoadInstruction.get_srv(), DM)) { + LoadInstruction.set_mipLevelOrSampleCount(HlslOP->GetI32Const(0)); + Modified = true; } } } diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl new file mode 100644 index 0000000000..e7129392ca --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalf.hlsl @@ -0,0 +1,19 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor,constant-red=0.5,constant-green=0.25,constant-blue=0.125,constant-alpha=1 | %FileCheck %s + +// A native half SV_Target lowers to dx.op.storeOutput.f16. + +// The override values are 0.5, 0.25, 0.125 and 1.0 as half: +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3800) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3400) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// Unused storeOutput overloads must not remain as external declarations. +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i16 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +half4 main() : SV_Target { + return half4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl new file mode 100644 index 0000000000..5638a098ee --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfFromCB.hlsl @@ -0,0 +1,36 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor,mod-mode=1 | %FileCheck %s + +// From-constant-buffer mode against a native half SV_Target0. The tools +// constant buffer is four 32-bit components; loaded values narrow to half. + +// CB return type is f32: +// CHECK: %dx.types.CBufRet.f32 = type { float, float, float, float } + +// Create handle: +// CHECK: %PIX_Constant_Color_CB_Handle = call %dx.types.Handle @dx.op.createHandle(i32 57, i8 2, i32 0, i32 0, i1 false) + +// Load the row: +// CHECK: %PIX_Constant_Color_Value = call %dx.types.CBufRet.f32 @dx.op.cbufferLoadLegacy.f32(i32 59, %dx.types.Handle %PIX_Constant_Color_CB_Handle, i32 0) + +// Extract components: +// CHECK: %PIX_Constant_Color_Value0 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 0 +// CHECK: %PIX_Constant_Color_Value1 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 1 +// CHECK: %PIX_Constant_Color_Value2 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 2 +// CHECK: %PIX_Constant_Color_Value3 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 3 + +// Narrow to half: +// CHECK: %PIX_Constant_Color_ValueNarrowed0 = fptrunc float %PIX_Constant_Color_Value0 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed1 = fptrunc float %PIX_Constant_Color_Value1 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed2 = fptrunc float %PIX_Constant_Color_Value2 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed3 = fptrunc float %PIX_Constant_Color_Value3 to half + +// Store SV_Target0: +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half %PIX_Constant_Color_ValueNarrowed0) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half %PIX_Constant_Color_ValueNarrowed1) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half %PIX_Constant_Color_ValueNarrowed2) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half %PIX_Constant_Color_ValueNarrowed3) + +[RootSignature("")] +half4 main() : SV_Target { + return half4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl new file mode 100644 index 0000000000..5d4462c463 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRT.hlsl @@ -0,0 +1,29 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor | %FileCheck %s + +// MRT: RTV0 is half4, RTV1 is float4. The override applies to SV_Target0 +// only. Default constant colour is 1.0 (0xH3C00 as half). + +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// RTV1 stays unchanged: +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 0, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 1, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 2, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 3, float 0.000000e+00) + +struct RTOut +{ + half4 h : SV_Target; + float4 c : SV_Target1; +}; + +[RootSignature("")] +RTOut main() { + RTOut rtOut; + rtOut.h = half4(0, 0, 0, 0); + rtOut.c = float4(0.f, 0.f, 0.f, 0.f); + return rtOut; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl new file mode 100644 index 0000000000..f8e625001e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorhalfMRTOnRTV1.hlsl @@ -0,0 +1,33 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor | %FileCheck %s + +// MRT: RTV0 is float4, RTV1 is half4. The override applies to SV_Target0 +// only; RTV1 stays unchanged. + +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 0, float 1.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 1, float 1.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 2, float 1.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 0, i32 0, i8 3, float 1.000000e+00) + +// RTV1 stays 0xH0000 (half 0.0): +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 0, half 0xH0000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 1, half 0xH0000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 2, half 0xH0000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 1, i32 0, i8 3, half 0xH0000) + +// Unused integer overloads must not remain as external declarations. +// CHECK-NOT: declare void @dx.op.storeOutput.i16 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +struct RTOut +{ + float4 c : SV_Target; + half4 h : SV_Target1; +}; + +[RootSignature("")] +RTOut main() { + RTOut rtOut; + rtOut.c = float4(0.f, 0.f, 0.f, 0.f); + rtOut.h = half4(0, 0, 0, 0); + return rtOut; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl new file mode 100644 index 0000000000..835fc3f013 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorint16.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -enable-16bit-types -Emain -Tps_6_2 %s | %opt -S -hlsl-dxil-constantColor,constant-red=8,constant-green=7,constant-blue=6,constant-alpha=5 | %FileCheck %s + +// A native uint16_t SV_Target lowers to dx.op.storeOutput.i16. + +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 0, i16 8) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 1, i16 7) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 2, i16 6) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 3, i16 5) + +// Unused storeOutput overloads must not remain as external declarations. +// CHECK-NOT: declare void @dx.op.storeOutput.f16 +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +uint16_t4 main() : SV_Target { + return uint16_t4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl new file mode 100644 index 0000000000..08d0fcae82 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecision.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor,constant-red=0.5,constant-green=0.25,constant-blue=0.125,constant-alpha=1 | %FileCheck %s + +// A min16float SV_Target lowers to dx.op.storeOutput.f16 at ps_6_0 without +// -enable-16bit-types. + +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3800) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3400) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3000) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i16 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +min16float4 main() : SV_Target { + return min16float4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl new file mode 100644 index 0000000000..1a15bdcc59 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionFromCB.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor,mod-mode=1 | %FileCheck %s + +// From-constant-buffer mode against a min16float SV_Target0 at ps_6_0 +// without -enable-16bit-types. The tools constant buffer is four 32-bit +// components; loaded values narrow to half. + +// CB return type is f32: +// CHECK: %dx.types.CBufRet.f32 = type { float, float, float, float } + +// Create handle: +// CHECK: %PIX_Constant_Color_CB_Handle = call %dx.types.Handle @dx.op.createHandle(i32 57, i8 2, i32 0, i32 0, i1 false) + +// Load the row: +// CHECK: %PIX_Constant_Color_Value = call %dx.types.CBufRet.f32 @dx.op.cbufferLoadLegacy.f32(i32 59, %dx.types.Handle %PIX_Constant_Color_CB_Handle, i32 0) + +// Extract components: +// CHECK: %PIX_Constant_Color_Value0 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 0 +// CHECK: %PIX_Constant_Color_Value1 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 1 +// CHECK: %PIX_Constant_Color_Value2 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 2 +// CHECK: %PIX_Constant_Color_Value3 = extractvalue %dx.types.CBufRet.f32 %PIX_Constant_Color_Value, 3 + +// Narrow to half: +// CHECK: %PIX_Constant_Color_ValueNarrowed0 = fptrunc float %PIX_Constant_Color_Value0 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed1 = fptrunc float %PIX_Constant_Color_Value1 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed2 = fptrunc float %PIX_Constant_Color_Value2 to half +// CHECK: %PIX_Constant_Color_ValueNarrowed3 = fptrunc float %PIX_Constant_Color_Value3 to half + +// Store SV_Target0: +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half %PIX_Constant_Color_ValueNarrowed0) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half %PIX_Constant_Color_ValueNarrowed1) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half %PIX_Constant_Color_ValueNarrowed2) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half %PIX_Constant_Color_ValueNarrowed3) + +[RootSignature("")] +min16float4 main() : SV_Target { + return min16float4(0, 0, 0, 0); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl new file mode 100644 index 0000000000..1addc05a48 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionMRT.hlsl @@ -0,0 +1,29 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor | %FileCheck %s + +// MRT at ps_6_0 without -enable-16bit-types: RTV0 is min16float4, RTV1 is +// float4. The override applies to SV_Target0 only. + +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 0, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 1, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 2, half 0xH3C00) +// CHECK: call void @dx.op.storeOutput.f16(i32 5, i32 0, i32 0, i8 3, half 0xH3C00) + +// RTV1 stays unchanged: +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 0, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 1, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 2, float 0.000000e+00) +// CHECK: call void @dx.op.storeOutput.f32(i32 5, i32 1, i32 0, i8 3, float 0.000000e+00) + +struct RTOut +{ + min16float4 h : SV_Target; + float4 c : SV_Target1; +}; + +[RootSignature("")] +RTOut main() { + RTOut rtOut; + rtOut.h = min16float4(0, 0, 0, 0); + rtOut.c = float4(0.f, 0.f, 0.f, 0.f); + return rtOut; +} diff --git a/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl new file mode 100644 index 0000000000..f18c542636 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/constantcolorminprecisionint.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -Emain -Tps_6_0 %s | %opt -S -hlsl-dxil-constantColor,constant-red=8,constant-green=7,constant-blue=6,constant-alpha=5 | %FileCheck %s + +// A min16int SV_Target lowers to dx.op.storeOutput.i16 at ps_6_0 without +// -enable-16bit-types. + +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 0, i16 8) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 1, i16 7) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 2, i16 6) +// CHECK: call void @dx.op.storeOutput.i16(i32 5, i32 0, i32 0, i8 3, i16 5) + +// CHECK-NOT: declare void @dx.op.storeOutput.f16 +// CHECK-NOT: declare void @dx.op.storeOutput.f32 +// CHECK-NOT: declare void @dx.op.storeOutput.i32 + +[RootSignature("")] +min16int4 main() : SV_Target { + return min16int4(1, 2, 3, 4); +} diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index e995a9f232..b6fc0a86e7 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -164,7 +164,10 @@ class PixTest : public ::testing::Test { TEST_METHOD(ToolsUav_ExtendingRootSignaturePreservesUnrelatedParameterFlags) TEST_METHOD(ConstantColor_UnusedIntOverloadIsErased) TEST_METHOD(ConstantColor_NoTargetOverloadsAreErased) + TEST_METHOD(ConstantColor_FromConstantBufferIsWellFormed) TEST_METHOD(RemoveDiscards_UnusedDiscardOverloadIsErased) + TEST_METHOD(ReduceMSAAToSingleSample_SM66) + TEST_METHOD(ReduceMSAAToSingleSample_HalfLoad) TEST_METHOD(OperationCacheCleanup_RemovesErasedFunctions) TEST_METHOD(DynamicResourceCleanup_VisitorStopsEarly) TEST_METHOD(MeshOutput_NoIndicesDeclarationIsErased) @@ -4026,6 +4029,121 @@ float4 main() : SV_Target "discard removal with no discard"); } +TEST_F(PixTest, ConstantColor_FromConstantBufferIsWellFormed) { + const char *source = R"x( +float4 main(float4 position : SV_Position) : SV_Target +{ + return position; +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_0", {L"-Od"}); + auto output = runSinglePass(compiled, L"-hlsl-dxil-constantColor,mod-mode=1"); + + // The CBuffer symbol must be a pointer to the struct so ValidateCBuffer + // can reach the annotation. + CComPtr pAssembler; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcAssembler, &pAssembler)); + CComPtr pAssembleResult; + VERIFY_SUCCEEDED( + pAssembler->AssembleToContainer(output.Module, &pAssembleResult)); + HRESULT assembleStatus; + VERIFY_SUCCEEDED(pAssembleResult->GetStatus(&assembleStatus)); + VERIFY_SUCCEEDED(assembleStatus); + + CComPtr pNewContainer; + VERIFY_SUCCEEDED(pAssembleResult->GetResult(&pNewContainer)); + + // The CBuffer resource record field 6 is size in bytes; a float4 row + // is 16 bytes. + auto lines = Tokenize(Disassemble(pNewContainer).c_str(), "\n"); + bool foundConstantColorCBuffer = false; + for (auto const &line : lines) { + if (line.find("!\"PIX_ConstantColorCBName\"") == std::string::npos) + continue; + auto fields = Tokenize(line.c_str(), ","); + VERIFY_IS_TRUE(fields.size() > 6); + // Field 1 is the global symbol; it must be a pointer to the CB struct. + VERIFY_ARE_NOT_EQUAL(std::string::npos, fields[1].find('*')); + VERIFY_ARE_EQUAL(16, atoi(fields[6].c_str() + fields[6].find("i32 ") + 4)); + foundConstantColorCBuffer = true; + } + VERIFY_IS_TRUE(foundConstantColorCBuffer); + + // The struct annotation names the float4 row in the reflection header. + bool foundStructAnnotation = false; + for (auto const &line : lines) { + if (line.find("struct PIX_ConstantColorCB_Type") != std::string::npos) + foundStructAnnotation = true; + } + VERIFY_IS_TRUE(foundStructAnnotation); + + verifyInstrumentedModuleIsValid(pNewContainer, + "constant-colour from constant buffer"); +} + +static void +VerifyMSAALoadSampleWasReduced(std::vector const &lines, + const char *textureLoadOverload, + const char *originalSampleIndex) { + bool foundTextureLoad = false; + for (auto const &line : lines) { + if (line.find(" call ") == std::string::npos || + line.find(textureLoadOverload) == std::string::npos) { + continue; + } + + foundTextureLoad = true; + VERIFY_ARE_EQUAL(std::string::npos, line.find(originalSampleIndex)); + VERIFY_ARE_NOT_EQUAL(std::string::npos, line.find(", i32 0,")); + } + VERIFY_IS_TRUE(foundTextureLoad); +} + +TEST_F(PixTest, ReduceMSAAToSingleSample_SM66) { + if (m_ver.SkipDxilVersion(1, 6)) + return; + + // SM 6.6 lowers the resource handle through annotateHandle. + const char *source = R"x( +Texture2DMS tex : register(t0); +float4 main(float4 position : SV_Position) : SV_Target +{ + return tex.Load(int2(position.xy), 3); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_6", {L"-Od"}); + auto output = runSinglePass(compiled, L"-hlsl-dxil-reduce-msaa-to-single"); + auto lines = Tokenize(Disassemble(output.Module).c_str(), "\n"); + + VerifyMSAALoadSampleWasReduced(lines, "dx.op.textureLoad.f32", ", i32 3,"); + verifyInstrumentedModuleIsValid(output.Module, + "MSAA reduction on SM 6.6 handle"); +} + +TEST_F(PixTest, ReduceMSAAToSingleSample_HalfLoad) { + if (m_ver.SkipDxilVersion(1, 2)) + return; + + // Texture2DMS.Load lowers to dx.op.textureLoad.f16. + const char *source = R"x( +Texture2DMS tex : register(t0); +float4 main(float4 position : SV_Position) : SV_Target +{ + half4 color = tex.Load(int2(position.xy), 2); + return float4(color); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"ps_6_2", + {L"-Od", L"-enable-16bit-types"}); + auto output = runSinglePass(compiled, L"-hlsl-dxil-reduce-msaa-to-single"); + auto lines = Tokenize(Disassemble(output.Module).c_str(), "\n"); + + VerifyMSAALoadSampleWasReduced(lines, "dx.op.textureLoad.f16", ", i32 2,"); + verifyInstrumentedModuleIsValid(output.Module, + "MSAA reduction on 16-bit texture load"); +} + TEST_F(PixTest, OperationCacheCleanup_RemovesErasedFunctions) { const char *Source = R"x( float4 main() : SV_Target From 2dbfd943f22b7d39d1c1296876c6d1603a2e88a9 Mon Sep 17 00:00:00 2001 From: Damyan Pepper Date: Thu, 1 Oct 2026 15:02:18 -0700 Subject: [PATCH 39/39] [PIX] Fix debug variable storage layout Correct shadow-storage offsets for arrays, aggregate padding, bitfields and matrices; use the true span for dynamically indexed alloca writes; handle deep GEP chains safely in IsAllocaRegisterWrite; and find embedded-array storage even when DXC renames the flattened global. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 40dc9de3-617e-4caf-ab0d-fba0a033ed93 --- .../DxilAnnotateWithVirtualRegister.cpp | 97 +++-- .../DxilDbgValueToDbgDeclare.cpp | 222 +++++++--- .../DxilDebugInstrumentation.cpp | 29 +- .../DbgValueToDbgDeclare_16bit_bitfields.hlsl | 27 ++ ...gValueToDbgDeclare_16bit_tail_padding.hlsl | 37 ++ ...ueToDbgDeclare_aggregate_tail_padding.hlsl | 36 ++ ...eToDbgDeclare_array_of_padded_structs.hlsl | 44 ++ .../DbgValueToDbgDeclare_constant_local.hlsl | 18 + ...ueToDbgDeclare_front_and_tail_padding.hlsl | 50 +++ ...alueToDbgDeclare_matrix_after_padding.hlsl | 37 ++ ...lueToDbgDeclare_mixed_width_bitfields.hlsl | 31 ++ .../DbgValueToDbgDeclare_multidim_array.hlsl | 19 + ...ToDbgDeclare_nested_aggregate_padding.hlsl | 46 ++ ...bugInstrumentation_dynamic_index_span.hlsl | 24 ++ tools/clang/unittests/HLSL/PixTest.cpp | 396 +++++++++++++++++- 15 files changed, 992 insertions(+), 121 deletions(-) create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl create mode 100644 tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl diff --git a/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp b/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp index 88f696b7fa..196da98c87 100644 --- a/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp +++ b/lib/DxilPIXPasses/DxilAnnotateWithVirtualRegister.cpp @@ -354,51 +354,70 @@ bool DxilAnnotateWithVirtualRegister::IsAllocaRegisterWrite( uint32_t precedingMemberCount = 0; auto *Alloca = llvm::dyn_cast(pGEP->getPointerOperand()); if (Alloca == nullptr) { - // In the case of vector types (floatN, matrixNxM), the pointer operand - // will actually point to another element pointer instruction. But this - // isn't a recursive thing- we only need to check these two levels. - if (auto *pPointerGEP = llvm::dyn_cast( - pGEP->getPointerOperand())) { - Alloca = - llvm::dyn_cast(pPointerGEP->getPointerOperand()); - if (Alloca == nullptr) { + // The pointer operand can itself be a GEP whenever the value being + // written is nested in more than one aggregate: a vector member of a + // struct (floatN, matrixNxM), or a struct member of a struct. Walk + // the whole chain of ancestor GEPs, outermost first, and require it + // to bottom out at an alloca. + llvm::SmallVector AncestorGEPs; + llvm::Value *PointerOperand = pGEP->getPointerOperand(); + while (auto *pAncestorGEP = + llvm::dyn_cast(PointerOperand)) { + AncestorGEPs.push_back(pAncestorGEP); + PointerOperand = pAncestorGEP->getPointerOperand(); + } + + Alloca = llvm::dyn_cast(PointerOperand); + if (Alloca == nullptr) { + return false; + } + + // Each level contributes the flattened count of whatever members precede + // the one it selects, so the offsets accumulate down the chain. + for (auto *pAncestorGEP : AncestorGEPs) { + auto *pStructType = llvm::dyn_cast( + pAncestorGEP->getPointerOperandType()->getPointerElementType()); + if (pStructType == nullptr) { + continue; + } + if (pAncestorGEP->getNumOperands() < 3) { + continue; + } + auto *pStructMember = + llvm::dyn_cast(pAncestorGEP->getOperand(2)); + if (pStructMember == nullptr) { + // A dynamically selected member has no constant offset; guessing + // zero would attribute the write to the wrong register. return false; } - // And of course the member we're after might not be at the beginning of - // any containing struct: - if (auto *pStructType = llvm::dyn_cast( - pPointerGEP->getPointerOperandType() - ->getPointerElementType())) { - auto *pStructMember = - llvm::dyn_cast(pPointerGEP->getOperand(2)); - uint64_t memberIndex = pStructMember->getLimitedValue(); - for (uint64_t i = 0; i < memberIndex; ++i) { - precedingMemberCount += - CountStructMembers(pStructType->getStructElementType(i)); - } + uint64_t memberIndex = pStructMember->getLimitedValue(); + if (memberIndex > pStructType->getStructNumElements()) { + return false; + } + for (uint64_t i = 0; i < memberIndex; ++i) { + precedingMemberCount += + CountStructMembers(pStructType->getStructElementType(i)); } + } - // And the source pointer may be a vector (floatn) type, - // and if so, that's another offset to consider. - llvm::Type *DestType = pGEP->getPointerOperand()->getType(); - // We expect this to be a pointer type (it's a GEP after all): - if (DestType->isPointerTy()) { - llvm::Type *PointedType = DestType->getPointerElementType(); - // Being careful to check num operands too in order to avoid false - // positives: - if (PointedType->isVectorTy() && pGEP->getNumOperands() == 3) { - // Fetch the second deref (in operand 2). - // (the first derefs the pointer to the "floatn", - // and the second denotes the index into the floatn.) - llvm::Value *vectorIndex = pGEP->getOperand(2); - if (auto *constIntIIndex = - llvm::cast(vectorIndex)) { - precedingMemberCount += constIntIIndex->getLimitedValue(); - } + // And the source pointer may be a vector (floatn) type, + // and if so, that's another offset to consider. + llvm::Type *DestType = pGEP->getPointerOperand()->getType(); + // We expect this to be a pointer type (it's a GEP after all): + if (DestType->isPointerTy()) { + llvm::Type *PointedType = DestType->getPointerElementType(); + // Being careful to check num operands too in order to avoid false + // positives: + if (PointedType->isVectorTy() && pGEP->getNumOperands() == 3) { + // Fetch the second deref (in operand 2). + // (the first derefs the pointer to the "floatn", + // and the second denotes the index into the floatn.) + llvm::Value *vectorIndex = pGEP->getOperand(2); + if (auto *constIntIIndex = + llvm::dyn_cast(vectorIndex)) { + precedingMemberCount += constIntIIndex->getLimitedValue(); } } - } else { - return false; } } diff --git a/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp b/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp index 9ddbe876b5..15a7e6c666 100644 --- a/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp +++ b/lib/DxilPIXPasses/DxilDbgValueToDbgDeclare.cpp @@ -145,6 +145,22 @@ class OffsetManager { } } + // Move the aligned offset forward without adding padding to the packed + // offset. Overlapping debug information cannot move storage mappings + // backward. + void AdvanceAlignedOffsetTo(OffsetInBits AlignedOffset) { + if (AlignedOffset > m_CurrentAlignedOffset) { + VALUE_TO_DECLARE_LOG("Advancing aligned offset from %d to %d", + m_CurrentAlignedOffset, AlignedOffset); + m_CurrentAlignedOffset = AlignedOffset; + } else if (AlignedOffset < m_CurrentAlignedOffset) { + VALUE_TO_DECLARE_LOG("Refusing to move aligned offset back from %d to %d", + m_CurrentAlignedOffset, AlignedOffset); + // Keep existing mappings monotonic when debug information overlaps. + return; + } + } + // Add is used to "add" an aggregate element (struct field, array element) // at the current aligned/packed offsets, bumping them by Ty's size. Offsets Add(llvm::DIBasicType *Ty, unsigned sizeOverride) { @@ -443,63 +459,61 @@ DescendTypeAndFindEmbeddedArrayElements(llvm::StringRef VariableName, } else if (auto *CompositeTy = llvm::dyn_cast(Ty)) { switch (CompositeTy->getTag()) { case llvm::dwarf::DW_TAG_array_type: { + // DXC flattens a multi-dimensional array to a single one-dimensional + // array in the module. The true array extent is the product of the + // dimensions. + uint64_t TotalElementCount = 1; + bool FoundSubrange = false; for (auto Element : CompositeTy->getElements()) { - // First element for an array is DISubrange - if (auto Subrange = llvm::dyn_cast(Element)) { - auto ElementTy = CompositeTy->getBaseType().resolve(EmptyMap); - if (auto *BasicTy = llvm::dyn_cast(ElementTy)) { - bool CorrectLowerOffset = AccumulatedMemberOffset == OffsetToSeek; - bool CorrectUpperOffset = - AccumulatedMemberOffset + - Subrange->getCount() * BasicTy->getSizeInBits() == - OffsetToSeek + SizeToSeek; - if (BasicTy != nullptr && CorrectLowerOffset && - CorrectUpperOffset) { - std::vector storage; - for (int64_t i = 0; i < Subrange->getCount(); ++i) { - auto ElementOffset = - AccumulatedMemberOffset + i * BasicTy->getSizeInBits(); - GlobalEmbeddedArrayElementStorage element; - element.Name = VariableName.str() + "." + std::to_string(i); - element.Offset = static_cast(ElementOffset); - element.Size = - static_cast(BasicTy->getSizeInBits()); - storage.push_back(std::move(element)); - } - return storage; - } - } + if (auto *Subrange = llvm::dyn_cast(Element)) { + TotalElementCount *= Subrange->getCount(); + FoundSubrange = true; + } + } + + if (!FoundSubrange) { + break; + } - // If we didn't succeed and return above, then we need to process each - // element in the array + auto ElementTy = CompositeTy->getBaseType().resolve(EmptyMap); + if (ElementTy == nullptr) { + break; + } + + if (auto *BasicTy = llvm::dyn_cast(ElementTy)) { + const bool CorrectLowerOffset = AccumulatedMemberOffset == OffsetToSeek; + const bool CorrectUpperOffset = + AccumulatedMemberOffset + + TotalElementCount * BasicTy->getSizeInBits() == + OffsetToSeek + SizeToSeek; + if (CorrectLowerOffset && CorrectUpperOffset) { std::vector storage; - for (int64_t i = 0; i < Subrange->getCount(); ++i) { - auto elementStorage = DescendTypeAndFindEmbeddedArrayElements( - VariableName, - AccumulatedMemberOffset + ElementTy->getSizeInBits() * i, - ElementTy, OffsetToSeek, SizeToSeek); - std::move(elementStorage.begin(), elementStorage.end(), - std::back_inserter(storage)); - } - if (!storage.empty()) { - return storage; + for (uint64_t i = 0; i < TotalElementCount; ++i) { + auto ElementOffset = + AccumulatedMemberOffset + i * BasicTy->getSizeInBits(); + GlobalEmbeddedArrayElementStorage element; + element.Name = VariableName.str() + "." + std::to_string(i); + element.Offset = static_cast(ElementOffset); + element.Size = static_cast(BasicTy->getSizeInBits()); + storage.push_back(std::move(element)); } + return storage; } } - for (auto Element : CompositeTy->getElements()) { - // First element for an array is DISubrange - if (auto Subrange = llvm::dyn_cast(Element)) { - auto ElementType = CompositeTy->getBaseType().resolve(EmptyMap); - for (int64_t i = 0; i < Subrange->getCount(); ++i) { - auto storage = DescendTypeAndFindEmbeddedArrayElements( - VariableName, - AccumulatedMemberOffset + ElementType->getSizeInBits() * i, - ElementType, OffsetToSeek, SizeToSeek); - if (!storage.empty()) { - return storage; - } - } - } + + // The array's elements are themselves aggregates, so descend into each of + // them in turn looking for the sought offset. + std::vector storage; + for (uint64_t i = 0; i < TotalElementCount; ++i) { + auto elementStorage = DescendTypeAndFindEmbeddedArrayElements( + VariableName, + AccumulatedMemberOffset + ElementTy->getSizeInBits() * i, ElementTy, + OffsetToSeek, SizeToSeek); + std::move(elementStorage.begin(), elementStorage.end(), + std::back_inserter(storage)); + } + if (!storage.empty()) { + return storage; } } break; case llvm::dwarf::DW_TAG_structure_type: @@ -554,11 +568,18 @@ GlobalStorageMap GatherGlobalEmbeddedArrayStorage(llvm::Module &M) { if (auto *DIGVDerivedType = llvm::dyn_cast(DIGVType)) { if (DIGVDerivedType->getTag() == llvm::dwarf::DW_TAG_member) { - // This type is embedded within the containing DIGSV type + // This type is embedded within the containing DIGSV type. + // A flattened multi-dimensional array member renames the module + // global but not the debug variable, so only the linkage name + // still identifies it. + llvm::StringRef GlobalName = DIGV->getLinkageName(); + if (GlobalName.empty()) { + GlobalName = DIGV->getName(); + } const llvm::DITypeIdentifierMap EmptyMap; auto *Ty = HLSLStruct->getType().resolve(EmptyMap); auto Storage = DescendTypeAndFindEmbeddedArrayElements( - DIGV->getName(), 0, Ty, DIGVDerivedType->getOffsetInBits(), + GlobalName, 0, Ty, DIGVDerivedType->getOffsetInBits(), DIGVDerivedType->getSizeInBits()); auto &ArrayStorage = ret[HLSLStruct].ArrayElementStorage; std::move(Storage.begin(), Storage.end(), @@ -617,7 +638,8 @@ bool DxilDbgValueToDbgDeclare::runOnModule(llvm::Module &M) { // lists. for (auto &instruction : instructions) { if (auto *Store = llvm::dyn_cast(instruction)) { - Changed = + // Preserve changes reported by every processed store. + Changed |= handleStoreIfDestIsGlobal(M, GlobalEmbeddedArrayStorage, Store); } } @@ -847,6 +869,33 @@ static bool IsDITypePointer(DIType *DTy, return false; } +static bool HasPointerBackedCompositeCopy(llvm::DbgValueInst *DbgValue, + llvm::DIType *Ty) { + const llvm::DITypeIdentifierMap EmptyMap; + llvm::DIType *UnaliasedTy = DITypePeelTypeAlias(Ty); + if (!llvm::isa(DbgValue->getValue()) || + !llvm::isa(UnaliasedTy)) { + return false; + } + + for (llvm::BasicBlock &Block : *DbgValue->getParent()->getParent()) { + for (llvm::Instruction &Instruction : Block) { + auto *OtherDbgValue = llvm::dyn_cast(&Instruction); + if (OtherDbgValue == nullptr || OtherDbgValue == DbgValue || + !llvm::isa(OtherDbgValue->getValue())) { + continue; + } + + llvm::DIType *OtherTy = + OtherDbgValue->getVariable()->getType().resolve(EmptyMap); + if (OtherTy != nullptr && DITypePeelTypeAlias(OtherTy) == UnaliasedTy) { + return true; + } + } + } + return false; +} + void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, llvm::DbgValueInst *DbgValue) { VALUE_TO_DECLARE_LOG("DbgValue named %s", DbgValue->getName().str().c_str()); @@ -908,6 +957,11 @@ void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, } } + if (HasPointerBackedCompositeCopy(DbgValue, Ty)) { + VALUE_TO_DECLARE_LOG("Using pointer-backed composite storage"); + return; + } + auto &Register = m_Registers[Variable]; if (Register == nullptr) { Register.reset(new VariableRegisters( @@ -932,6 +986,10 @@ void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, const OffsetInBits InitialOffset = PackedOffsetFromVar; auto *insertPt = llvm::dyn_cast(ValueFromDbgInst); + if (insertPt == nullptr) { + // Constants and arguments are available at the dbg.value location. + insertPt = DbgValue; + } if (insertPt != nullptr && !llvm::isa(insertPt)) { insertPt = insertPt->getNextNode(); // Drivers may crash if phi nodes aren't always at the top of a block, @@ -965,11 +1023,28 @@ void DxilDbgValueToDbgDeclare::handleDbgValue(llvm::Module &M, continue; } - if (AllocaInst->getAllocatedType()->getArrayElementType() == - VO.m_V->getType()) { - auto *GEP = B.CreateGEP(AllocaInst, {Zero, Zero}); - B.CreateStore(VO.m_V, GEP); + llvm::Type *ShadowElementType = + AllocaInst->getAllocatedType()->getArrayElementType(); + llvm::Value *ValueToStore = VO.m_V; + if (ShadowElementType != ValueToStore->getType()) { + // Emit a bitcast to match the shadow alloca element type when the + // shader reinterprets the bits without converting them. + const llvm::DataLayout &DataLayout = M.getDataLayout(); + const bool SameWidth = + !ShadowElementType->isAggregateType() && + !ValueToStore->getType()->isAggregateType() && + !ShadowElementType->isPointerTy() && + !ValueToStore->getType()->isPointerTy() && + DataLayout.getTypeSizeInBits(ShadowElementType) == + DataLayout.getTypeSizeInBits(ValueToStore->getType()); + if (!SameWidth) { + continue; + } + ValueToStore = B.CreateBitCast(ValueToStore, ShadowElementType); } + + auto *GEP = B.CreateGEP(AllocaInst, {Zero, Zero}); + B.CreateStore(ValueToStore, GEP); } } } @@ -1158,7 +1233,13 @@ void VariableRegisters::PopulateAllocaMap(llvm::DIType *Ty) { case llvm::dwarf::DW_TAG_enumeration_type: { auto *baseType = CompositeTy->getBaseType().resolve(EmptyMap); if (baseType != nullptr) { + const OffsetInBits EnumerationStart = + m_Offsets.GetCurrentAlignedOffset(); PopulateAllocaMap(baseType); + // Advance to the enumeration's own declared size. + m_Offsets.AdvanceAlignedOffsetTo( + EnumerationStart + + static_cast(CompositeTy->getSizeInBits())); } else { m_Offsets.AlignToAndAddUnhandledType(CompositeTy); } @@ -1228,9 +1309,10 @@ void VariableRegisters::PopulateAllocaMap_BasicType(llvm::DIBasicType *Ty, auto *Storage = GetMetadataAsValue(llvm::ValueAsMetadata::get(Alloca)); auto *Variable = GetMetadataAsValue(m_Variable); - auto *Expression = GetMetadataAsValue( - GetDIExpression(Ty, sizeOverride == 0 ? offsets.Aligned : offsets.Packed, - GetVariableSizeInbits(m_Variable), sizeOverride)); + // Describe the aligned offset in the bit_piece so it agrees with the + // debug-info field's declared offset. + auto *Expression = GetMetadataAsValue(GetDIExpression( + Ty, offsets.Aligned, GetVariableSizeInbits(m_Variable), sizeOverride)); auto *DbgDeclare = m_B.CreateCall(m_DbgDeclareFn, {Storage, Variable, Expression}); DbgDeclare->setDebugLoc(m_dbgLoc); @@ -1261,7 +1343,6 @@ void VariableRegisters::PopulateAllocaMap_ArrayType(llvm::DICompositeType *Ty) { } const SizeInBits ArraySizeInBits = Ty->getSizeInBits(); - (void)ArraySizeInBits; const llvm::DITypeIdentifierMap EmptyMap; llvm::DIType *ElementTy = Ty->getBaseType().resolve(EmptyMap); @@ -1274,18 +1355,25 @@ void VariableRegisters::PopulateAllocaMap_ArrayType(llvm::DICompositeType *Ty) { // in bits. m_Offsets.AlignTo(ElementTy); + const OffsetInBits ArrayStart = m_Offsets.GetCurrentAlignedOffset(); + for (unsigned i = 0; i < NumElements; ++i) { // This is only needed if ElementTy's size is not a multiple of // its natural alignment. m_Offsets.AlignTo(ElementTy); PopulateAllocaMap(ElementTy); } + + // The elements only account for the bits they occupy, which stops short + // of the array's real end for a padded element type. Advance to the end. + m_Offsets.AdvanceAlignedOffsetTo(ArrayStart + ArraySizeInBits); } void VariableRegisters::PopulateAllocaMap_StructType( llvm::DICompositeType *Ty) { VALUE_TO_DECLARE_LOG("Struct type : %s, size %d", Ty->getName().str().c_str(), Ty->getSizeInBits()); + const SizeInBits StructSizeInBits = Ty->getSizeInBits(); std::map SortedMembers; if (!SortMembers(Ty, &SortedMembers)) { m_Offsets.AlignToAndAddUnhandledType(Ty); @@ -1294,7 +1382,6 @@ void VariableRegisters::PopulateAllocaMap_StructType( m_Offsets.AlignTo(Ty); const OffsetInBits StructStart = m_Offsets.GetCurrentAlignedOffset(); - (void)StructStart; const llvm::DITypeIdentifierMap EmptyMap; for (auto OffsetAndMember : SortedMembers) { @@ -1310,6 +1397,10 @@ void VariableRegisters::PopulateAllocaMap_StructType( // than the type in which it resides). If we were to take // the base type, then the information about the member's // size would be lost + // + // The AlignTo above is a no-op for a bitfield, so snap to the declared + // offset here to ensure it aligns with the debug info. + m_Offsets.AdvanceAlignedOffsetTo(StructStart + OffsetAndMember.first); PopulateAllocaMap(OffsetAndMember.second); } else { if (OffsetAndMember.second->getAlignInBits() == @@ -1326,6 +1417,11 @@ void VariableRegisters::PopulateAllocaMap_StructType( } } } + + // The members between them only account for the bits they occupy, which stops + // short of the struct's real end whenever the struct's alignment requires + // tail padding. Advance to the struct's full size. + m_Offsets.AdvanceAlignedOffsetTo(StructStart + StructSizeInBits); } // HLSL Change: remove unused function diff --git a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp index a40acfe860..d533e0c95b 100644 --- a/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp +++ b/lib/DxilPIXPasses/DxilDebugInstrumentation.cpp @@ -235,6 +235,7 @@ struct InstructionAndType { DebugShaderModifierRecordType Type; std::uint32_t RegisterNumber; std::uint32_t AllocaBase; + std::uint32_t AllocaRegisterSize = 0; Value *AllocaWriteIndex = nullptr; std::optional ConstantAllocaStoreValue; }; @@ -993,11 +994,10 @@ std::optional DxilDebugInstrumentation::addStoreStepDebugEntry(BuilderContext *BC, StoreInst *Inst) { std::uint32_t ValueOrdinalBase; - std::uint32_t UnusedValueOrdinalSize; + std::uint32_t ValueOrdinalSize; llvm::Value *ValueOrdinalIndex; - if (!pix_dxil::PixAllocaRegWrite::FromInst(Inst, &ValueOrdinalBase, - &UnusedValueOrdinalSize, - &ValueOrdinalIndex)) { + if (!pix_dxil::PixAllocaRegWrite::FromInst( + Inst, &ValueOrdinalBase, &ValueOrdinalSize, &ValueOrdinalIndex)) { return std::nullopt; } @@ -1019,6 +1019,7 @@ DxilDebugInstrumentation::addStoreStepDebugEntry(BuilderContext *BC, ret.Type = *Type; ret.RegisterNumber = RegNum; ret.AllocaBase = ValueOrdinalBase; + ret.AllocaRegisterSize = ValueOrdinalSize; ret.AllocaWriteIndex = ValueOrdinalIndex; return ret; } @@ -1029,6 +1030,7 @@ DxilDebugInstrumentation::addStoreStepDebugEntry(BuilderContext *BC, ret.InstructionOrdinal = InstNum; ret.Type = *Type; ret.AllocaBase = ValueOrdinalBase; + ret.AllocaRegisterSize = ValueOrdinalSize; ret.AllocaWriteIndex = ValueOrdinalIndex; switch (ValueAsConst->getType()->getTypeID()) { @@ -1356,21 +1358,18 @@ DxilDebugInstrumentation::FindInstrumentableInstructionsInBlock( IndexingToken = "s"; // static indexing, no debug output required } else { IndexingToken = "d"; // dynamic indexing - int MaxArraySize = 1; - if (auto *Store = dyn_cast(&Inst)) { - if (auto *GEP = - dyn_cast(Store->getPointerOperand())) { - if (auto *Alloca = - dyn_cast(GEP->getPointerOperand())) { - MaxArraySize = - Alloca->getAllocatedType()->getArrayNumElements(); - } - } - } + // The register span for a dynamic write comes from the + // !pix-alloca-reg-write metadata the annotation pass attached to + // this instruction, so it always matches the virtual-register + // numbering the annotation pass assigned. + uint32_t MaxArraySize = std::max(1u, IandT->AllocaRegisterSize); RegisterOrStaticIndex = std::to_string(IandT->AllocaBase) + "-" + std::to_string(MaxArraySize); DebugOutputForThisInstruction.ValueToWriteToDebugMemory = IandT->AllocaWriteIndex; + // Dynamic alloca records store the i32 index as a four-byte payload. + DebugOutputForThisInstruction.ValueType = + DebugShaderModifierRecordTypeDXILStepUint32; } } else { IndexingToken = "a"; // meaning an SSA assignment diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl new file mode 100644 index 0000000000..f60df66212 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_bitfields.hlsl @@ -0,0 +1,27 @@ +// RUN: %dxc -Tcs_6_6 -enable-16bit-types -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify that 16-bit and 32-bit bitfields use their declared storage units. + +RWByteAddressBuffer RawUAV : register(u0); + +struct HalfBitfield +{ + uint16_t Small : 5; + uint32_t Wide : 20; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 5) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 32, 20) + +// CHECK: store i16 %{{[^,]+}}, i16* +// CHECK: store i32 %{{[^,]+}}, i32* + +[numthreads(1, 1, 1)] +void main() +{ + HalfBitfield bitfield; + bitfield.Small = (uint16_t)RawUAV.Load(2 * 4); + bitfield.Wide = RawUAV.Load(3 * 4); + + RawUAV.Store(0, bitfield.Small + bitfield.Wide); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl new file mode 100644 index 0000000000..f0a5f5291d --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_16bit_tail_padding.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Tcs_6_6 -enable-16bit-types -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify a 16-bit member after a tail-padded 64-bit struct. + +RWByteAddressBuffer RawUAV : register(u0); + +struct HalfTail +{ + float Wide; + float16_t Small; +}; + +struct Holder +{ + HalfTail Padded; + float16_t Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 32, 16) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 16) + +// CHECK: store float +// CHECK: store half +// CHECK: store half + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Padded.Wide = (float)RawUAV.Load(2 * 4); + holder.Padded.Small = (float16_t)RawUAV.Load(3 * 4); + holder.Trailing = (float16_t)RawUAV.Load(4 * 4); + + RawUAV.Store(0, holder.Padded.Wide + (float)holder.Padded.Small + + (float)holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl new file mode 100644 index 0000000000..4180e594f9 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_aggregate_tail_padding.hlsl @@ -0,0 +1,36 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify a member after a 128-bit struct with 32 bits of tail padding. + +RWByteAddressBuffer RawUAV : register(u0); + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + TailPadded Padded; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 32) + +// CHECK: store double +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Padded.Wide = (double)RawUAV.Load(2 * 4); + holder.Padded.Narrow = (float)RawUAV.Load(3 * 4); + holder.Trailing = (float)RawUAV.Load(7 * 4); + + RawUAV.Store(0, (float)holder.Padded.Wide + holder.Padded.Narrow + holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl new file mode 100644 index 0000000000..001a1353cc --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_array_of_padded_structs.hlsl @@ -0,0 +1,44 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify an array of 128-bit padded structs followed by another member. + +RWByteAddressBuffer RawUAV : register(u0); + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + TailPadded Elements[2]; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 256, 32) + +// CHECK: store double +// CHECK: store float +// CHECK: store double +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Elements[0].Wide = (double)RawUAV.Load(2 * 4); + holder.Elements[0].Narrow = (float)RawUAV.Load(3 * 4); + holder.Elements[1].Wide = (double)RawUAV.Load(4 * 4); + holder.Elements[1].Narrow = (float)RawUAV.Load(5 * 4); + holder.Trailing = (float)RawUAV.Load(7 * 4); + + RawUAV.Store(0, (float)holder.Elements[0].Wide + holder.Elements[0].Narrow + + (float)holder.Elements[1].Wide + holder.Elements[1].Narrow + + holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl new file mode 100644 index 0000000000..38d82b5b92 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_constant_local.hlsl @@ -0,0 +1,18 @@ +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +RWByteAddressBuffer RawUAV : register(u0); + +[numthreads(1, 1, 1)] +void main() +{ + float bias = 0.5; + bool hitFlag = true; + RawUAV.Store(0, asuint(bias + (hitFlag ? 2.0 : 3.0))); +} + +// CHECK: %[[hitFlag:.*]] = alloca [1 x i32] +// CHECK: %[[bias:.*]] = alloca [1 x float] +// CHECK: %[[bias_gep:.*]] = getelementptr [1 x float], [1 x float]* %[[bias]], i32 0, i32 0 +// CHECK: store float 5.000000e-01, float* %[[bias_gep]] +// CHECK: %[[hitFlag_gep:.*]] = getelementptr [1 x i32], [1 x i32]* %[[hitFlag]], i32 0, i32 0 +// CHECK: store i32 1, i32* %[[hitFlag_gep]] diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl new file mode 100644 index 0000000000..fc5a6e216b --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_front_and_tail_padding.hlsl @@ -0,0 +1,50 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify declared offsets for front-padded and tail-padded structs. + +RWByteAddressBuffer RawUAV : register(u0); + +struct FrontPadded +{ + float Narrow; + double Wide; +}; + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + FrontPadded Front; + TailPadded Tail; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 256, 32) + +// CHECK: store float +// CHECK: store double +// CHECK: store double +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Front.Narrow = (float)RawUAV.Load(2 * 4); + holder.Front.Wide = (double)RawUAV.Load(3 * 4); + holder.Tail.Wide = (double)RawUAV.Load(4 * 4); + holder.Tail.Narrow = (float)RawUAV.Load(5 * 4); + holder.Trailing = (float)RawUAV.Load(6 * 4); + + RawUAV.Store(0, holder.Front.Narrow + (float)holder.Front.Wide + + (float)holder.Tail.Wide + holder.Tail.Narrow + holder.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl new file mode 100644 index 0000000000..d678350e3d --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_matrix_after_padding.hlsl @@ -0,0 +1,37 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify matrix offsets after a 128-bit tail-padded struct. + +RWByteAddressBuffer RawUAV : register(u0); + +struct TailPadded +{ + double Wide; + float Narrow; +}; + +struct Holder +{ + TailPadded Padded; + float2x2 Mat; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 160, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 224, 32) + +[numthreads(1, 1, 1)] +void main() +{ + Holder holder; + holder.Padded.Wide = (double)RawUAV.Load(2 * 4); + holder.Padded.Narrow = (float)RawUAV.Load(3 * 4); + holder.Mat = float2x2(RawUAV.Load(4 * 4), RawUAV.Load(5 * 4), + RawUAV.Load(6 * 4), RawUAV.Load(7 * 4)); + + RawUAV.Store(0, (float)holder.Padded.Wide + holder.Padded.Narrow + + holder.Mat._11 + holder.Mat._22); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl new file mode 100644 index 0000000000..1a72d36ef4 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_mixed_width_bitfields.hlsl @@ -0,0 +1,31 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify that mixed-width bitfields use their declared storage units. + +RWByteAddressBuffer RawUAV : register(u0); + +struct MixedWidthBitfield +{ + uint32_t Leading : 5; + uint64_t Middle : 59; + uint32_t Trailing : 5; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 5) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 59) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 5) + +// CHECK: store i32 %{{[^,]+}}, i32* +// CHECK: store i64 %{{[^,]+}}, i64* +// CHECK: store i32 %{{[^,]+}}, i32* + +[numthreads(1, 1, 1)] +void main() +{ + MixedWidthBitfield bitfield; + bitfield.Leading = RawUAV.Load(9 * 4); + bitfield.Middle = RawUAV.Load(21 * 4); + bitfield.Trailing = RawUAV.Load(13 * 4); + + RawUAV.Store(0, (uint)(bitfield.Leading + bitfield.Middle + bitfield.Trailing)); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl new file mode 100644 index 0000000000..e4eb97405a --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_multidim_array.hlsl @@ -0,0 +1,19 @@ +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +static float MyArray[2][2] = { + { 1.0, 2.0 }, + { 3.0, 4.0 } +}; + +RWByteAddressBuffer RawUAV : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + RawUAV.Store(0, asuint(MyArray[tid.x][tid.y])); +} + +// Verify stores for every flattened element of the multidimensional array. +// CHECK: store float 2.000000e+00, float* +// CHECK: store float 3.000000e+00, float* +// CHECK: store float 4.000000e+00, float* diff --git a/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl new file mode 100644 index 0000000000..fa9ed43a92 --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DbgValueToDbgDeclare_nested_aggregate_padding.hlsl @@ -0,0 +1,46 @@ +// RUN: %dxc -Tcs_6_6 -Emain /Od /Zi %s | %opt -S -dxil-dbg-value-to-dbg-declare | %FileCheck %s + +// Verify declared offsets across nested tail-padded aggregates. + +RWByteAddressBuffer RawUAV : register(u0); + +struct Leaf +{ + double Wide; + float Narrow; +}; + +struct Middle +{ + Leaf Nested; + float AfterNested; +}; + +struct Root +{ + Middle Inner; + float Trailing; +}; + +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 0, 64) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 64, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 128, 32) +// CHECK: dbg.declare{{.*}}!DIExpression(DW_OP_bit_piece, 192, 32) + +// CHECK: store double +// CHECK: store float +// CHECK: store float +// CHECK: store float + +[numthreads(1, 1, 1)] +void main() +{ + Root root; + root.Inner.Nested.Wide = (double)RawUAV.Load(2 * 4); + root.Inner.Nested.Narrow = (float)RawUAV.Load(3 * 4); + root.Inner.AfterNested = (float)RawUAV.Load(4 * 4); + root.Trailing = (float)RawUAV.Load(5 * 4); + + RawUAV.Store(0, (float)root.Inner.Nested.Wide + root.Inner.Nested.Narrow + + root.Inner.AfterNested + root.Trailing); +} diff --git a/tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl b/tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl new file mode 100644 index 0000000000..65c20c4f7e --- /dev/null +++ b/tools/clang/test/HLSLFileCheck/pix/DebugInstrumentation_dynamic_index_span.hlsl @@ -0,0 +1,24 @@ +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-annotate-with-virtual-regs -hlsl-dxil-debug-instrumentation,UAVSize=1048576 | %FileCheck %s -check-prefix=SIZE +// RUN: %dxc -T cs_6_0 -Od -Zi %s | %opt -S -dxil-annotate-with-virtual-regs -hlsl-dxil-debug-instrumentation,UAVSize=1048576 | %FileCheck %s -check-prefix=NO-OLD + +RWByteAddressBuffer RawUAV : register(u0); + +[numthreads(1, 1, 1)] void main() { + double local_array[4]; + uint index = RawUAV.Load(0); + double value = asdouble(RawUAV.Load(4), RawUAV.Load(8)); + if (index < 4) { + local_array[index] = value; + } + RawUAV.Store(12, asuint((float)local_array[0])); +} + +// The store block reserves a 12-byte header and a 4-byte index payload. +// SIZE-LABEL: define void @main() +// SIZE: getelementptr inbounds [4 x double], [4 x double]* %{{[a-zA-Z0-9._]+}}, i32 0, i32 %{{[a-zA-Z0-9._]+}} +// SIZE: call i32 @dx.op.atomicBinOp.i32(i32 78, %dx.types.Handle {{.*}}, i32 0, i32 {{.*}}, i32 undef, i32 undef, i32 16) +// SIZE-LABEL: declare double @dx.op.makeDouble.f64 + +// NO-OLD-LABEL: define void @main() +// NO-OLD-NOT: i32 undef, i32 undef, i32 20) +// NO-OLD-LABEL: declare double @dx.op.makeDouble.f64 diff --git a/tools/clang/unittests/HLSL/PixTest.cpp b/tools/clang/unittests/HLSL/PixTest.cpp index b6fc0a86e7..17d5eb2a80 100644 --- a/tools/clang/unittests/HLSL/PixTest.cpp +++ b/tools/clang/unittests/HLSL/PixTest.cpp @@ -38,6 +38,7 @@ #include "dxc/DXIL/DxilModule.h" #include "dxc/DXIL/DxilOperations.h" #include "dxc/DXIL/DxilSubobject.h" +#include "dxc/DxilPIXPasses/DxilPIXPasses.h" #include "dxc/Test/DxcTestUtils.h" #include "dxc/Test/HLSLTestData.h" @@ -53,6 +54,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringSwitch.h" +#include "llvm/AsmParser/Parser.h" #include "llvm/Bitcode/ReaderWriter.h" #include "llvm/IR/Constants.h" #include "llvm/IR/DebugInfo.h" @@ -63,10 +65,12 @@ #include "llvm/IR/Module.h" #include "llvm/IR/ModuleSlotTracker.h" #include "llvm/IR/Operator.h" +#include "llvm/Pass.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MSFileSystem.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" +#include "llvm/Support/SourceMgr.h" #include #include <../lib/DxilDia/DxcPixLiveVariables_FragmentIterator.h> @@ -149,6 +153,11 @@ class PixTest : public ::testing::Test { TEST_METHOD(PixStructAnnotation_Inheritance) TEST_METHOD(PixStructAnnotation_ResourceAsMember) TEST_METHOD(PixStructAnnotation_WheresMyDbgValue) + TEST_METHOD(DbgValueToDbgDeclare_BackwardLayout) + TEST_METHOD(DebugInstrumentation_DynamicIndexSpanMatchesAllocaRegisterCount) + TEST_METHOD(PixDbgValueToDbgDeclare_MultiDimensionalStaticGlobalArray) + TEST_METHOD(AllocaRegisterWrite_DeepAggregateChainIsAnnotated) + TEST_METHOD(EntryBlockInjection_HandlesLabelledAndUnlabelledFirstBlock) TEST_METHOD(VirtualRegisters_InstructionCounts) TEST_METHOD(VirtualRegisters_AlignedOffsets) @@ -313,6 +322,42 @@ class PixTest : public ::testing::Test { std::vector Lines; }; + // Runs the virtual-register annotation pass over textual IR and returns the + // pass report. Textual IR builds a module shape that HLSL does not express. + std::vector RunAnnotationPassOnText(const std::string &irText) { + CComPtr pSource; + CreateBlobFromText(m_dllSupport, irText.c_str(), &pSource); + + CComPtr pOptimizer; + VERIFY_SUCCEEDED( + m_dllSupport.CreateInstance(CLSID_DxcOptimizer, &pOptimizer)); + std::vector Options; + Options.push_back(L"-S"); + Options.push_back(L"-opt-mod-passes"); + Options.push_back(L"-dxil-annotate-with-virtual-regs"); + + CComPtr pOptimizedModule; + CComPtr pText; + VERIFY_SUCCEEDED(pOptimizer->RunOptimizer( + pSource, Options.data(), Options.size(), &pOptimizedModule, &pText)); + + return Tokenize(BlobToUtf8(pText).c_str(), "\n"); + } + + // Replaces the one occurrence of needle, and fails the test when the text + // does not hold exactly one. + static std::string ReplaceOnlyOccurrence(const std::string &text, + const std::string &needle, + const std::string &replacement) { + auto position = text.find(needle); + VERIFY_IS_TRUE(position != std::string::npos); + VERIFY_IS_TRUE(text.find(needle, position + needle.size()) == + std::string::npos); + std::string result = text; + result.replace(position, needle.size(), replacement); + return result; + } + SinglePassOutput runSinglePass(IDxcBlob *Dxil, LPCWSTR PassOption) { CComPtr Optimizer; VERIFY_SUCCEEDED( @@ -2213,9 +2258,9 @@ void main() auto Testables = TestStructAnnotationCase(hlsl, optimization); - // 2 in unoptimized case (one for each instance of smallPayload) - // 1 in optimized case (cuz p2 aliases over p) - VERIFY_IS_TRUE(Testables.OffsetAndSizes.size() >= 1); + // Each unoptimized source variable has storage. Optimized copies alias. + const size_t ExpectedCount = choice.IsOptimized ? 1u : 2u; + VERIFY_ARE_EQUAL(ExpectedCount, Testables.OffsetAndSizes.size()); for (const auto &os : Testables.OffsetAndSizes) { VERIFY_ARE_EQUAL(1u, os.countOfMembers); @@ -2223,8 +2268,76 @@ void main() VERIFY_ARE_EQUAL(32u, os.size); } - VERIFY_ARE_EQUAL(1u, Testables.AllocaWrites.size()); + VERIFY_ARE_EQUAL(ExpectedCount, Testables.AllocaWrites.size()); + for (size_t i = 0; i < ExpectedCount; ++i) { + ValidateAllocaWrite(Testables.AllocaWrites, i, "dummy"); + } + } +} + +TEST_F(PixTest, DbgValueToDbgDeclare_BackwardLayout) { + const char *IR = R"( + %BadStruct = type { i64, i32 } + + define void @main() !dbg !5 { + entry: + %var = alloca %BadStruct, align 4 + call void @llvm.dbg.value(metadata %BadStruct* %var, i64 0, metadata !10, metadata !15), !dbg !16 + ret void + } + + declare void @llvm.dbg.value(metadata, i64, metadata, metadata) + + !llvm.dbg.cu = !{!0} + !llvm.module.flags = !{!3, !4} + + !0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: 1, subprograms: !2) + !1 = !DIFile(filename: "test.hlsl", directory: "/") + !2 = !{!5} + !3 = !{i32 2, !"Dwarf Version", i32 4} + !4 = !{i32 2, !"Debug Info Version", i32 3} + !5 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 1, type: !6, isLocal: false, isDefinition: true, scopeLine: 1, flags: DIFlagPrototyped, isOptimized: false, function: void ()* @main) + !6 = !DISubroutineType(types: !7) + !7 = !{null} + !8 = !DIBasicType(name: "int64", size: 64, align: 32, encoding: DW_ATE_signed) + !9 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed) + !10 = !DILocalVariable(tag: DW_TAG_auto_variable, name: "var", scope: !5, file: !1, line: 2, type: !11) + !11 = !DICompositeType(tag: DW_TAG_structure_type, name: "BadStruct", file: !1, line: 1, size: 96, align: 32, elements: !12) + !12 = !{!13, !14} + !13 = !DIDerivedType(tag: DW_TAG_member, name: "First", scope: !11, file: !1, line: 2, baseType: !8, size: 64, align: 32, offset: 0) + !14 = !DIDerivedType(tag: DW_TAG_member, name: "Second", scope: !11, file: !1, line: 3, baseType: !9, size: 16, align: 32, offset: 32) + !15 = !DIExpression() + !16 = !DILocation(line: 2, column: 1, scope: !5) + )"; + + llvm::LLVMContext Context; + llvm::SMDiagnostic Error; + std::unique_ptr Module = + llvm::parseAssemblyString(IR, Error, Context); + VERIFY_IS_NOT_NULL(Module.get()); + + std::unique_ptr Pass( + llvm::createDxilDbgValueToDbgDeclarePass()); + VERIFY_IS_TRUE(Pass->runOnModule(*Module)); + + std::vector> Pieces; + for (llvm::BasicBlock &Block : *Module->getFunction("main")) { + for (llvm::Instruction &Instruction : Block) { + if (auto *Declare = llvm::dyn_cast(&Instruction)) { + llvm::DIExpression *Expression = Declare->getExpression(); + VERIFY_IS_TRUE(Expression->isBitPiece()); + Pieces.emplace_back(Expression->getBitPieceOffset(), + Expression->getBitPieceSize()); + } + } } + + std::sort(Pieces.begin(), Pieces.end()); + VERIFY_ARE_EQUAL(size_t(2), Pieces.size()); + VERIFY_ARE_EQUAL(uint64_t(0), Pieces[0].first); + VERIFY_ARE_EQUAL(uint64_t(64), Pieces[0].second); + VERIFY_ARE_EQUAL(uint64_t(64), Pieces[1].first); + VERIFY_ARE_EQUAL(uint64_t(16), Pieces[1].second); } TEST_F(PixTest, PixStructAnnotation_MixedSizes) { @@ -5184,3 +5297,278 @@ float4 main(float4 pos : SV_Position) : SV_Target verifyInstrumentedModuleIsValid( output.blob, "shader access tracking of a dynamically indexed resource"); } + +// Pulls the register span out of every dynamically-indexed alloca write the +// debug instrumentation pass reported. The per-block records it emits are +// semicolon-separated and a dynamic alloca write looks like +// +// ,,,d,- +// +// where the span is how many virtual registers the write could land in. +static std::vector +FindDynamicAllocaWriteSpans(std::vector const &passOutputLines) { + std::vector spans; + for (auto const &line : passOutputLines) { + for (auto const &record : Split(line, ';')) { + auto tokens = Split(record, ','); + if (tokens.size() < 5 || tokens[3] != "d") { + continue; + } + auto const dash = tokens[4].find('-'); + if (dash == std::string::npos) { + continue; + } + spans.push_back(atoi(tokens[4].substr(dash + 1).c_str())); + } + } + return spans; +} + +// PIX clamps a dynamic index to the span this record reports, so a span that +// undercounts the alloca hides every element past it. The span comes from the +// !pix-alloca-reg-write metadata the annotation pass attaches to the +// instruction, not from the alloca's LLVM array length, so it always matches +// the virtual-register numbering. +// +// DXC's SROA flattens every aggregate the front end emits, so today's shapes +// keep both derivations in agreement. This test guards against that ceasing +// to be true. +TEST_F(PixTest, + DebugInstrumentation_DynamicIndexSpanMatchesAllocaRegisterCount) { + struct Case { + char const *description; + char const *source; + int expectedSpan; + }; + + const Case cases[] = { + {"one-dimensional float array", R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + float values[8]; + for (uint i = 0; i < 8; ++i) values[i] = 0; + values[RawUAV.Load(0)] = 7; + RawUAV.Store(4, asuint(values[RawUAV.Load(8)])); +})x", + 8}, + {"two-dimensional array is flattened to one register run", R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + float m[4][4]; + for (uint i = 0; i < 4; ++i) for (uint j = 0; j < 4; ++j) m[i][j] = 0; + m[RawUAV.Load(0)][RawUAV.Load(4)] = 7; + RawUAV.Store(8, asuint(m[RawUAV.Load(12)][RawUAV.Load(16)])); +})x", + 16}, + {"array member of a struct", R"x( +RWByteAddressBuffer RawUAV : register(u0); +struct Container { float before; float values[8]; float after; }; +[numthreads(1, 1, 1)] +void main() +{ + Container c; + c.before = 1; + c.after = 2; + for (uint i = 0; i < 8; ++i) c.values[i] = 0; + c.values[RawUAV.Load(0)] = 7; + RawUAV.Store(4, asuint(c.values[RawUAV.Load(8)] + c.before + c.after)); +})x", + 8}, + {"dynamically indexed vector", R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + float3 v = float3(1, 2, 3); + v[RawUAV.Load(0)] = 7; + RawUAV.Store(4, asuint(v.x + v.y + v.z)); +})x", + 3}, + }; + + for (auto const &testCase : cases) { + WEX::Logging::Log::Comment( + WEX::Common::String().Format(L"%S", testCase.description)); + + auto compiled = Compile(m_dllSupport, testCase.source, L"cs_6_0", {L"-Od"}); + auto output = RunDebugPass(compiled); + auto spans = FindDynamicAllocaWriteSpans(output.lines); + + // If DXC ever stops emitting a dynamically-indexed alloca store for these + // shaders the test would otherwise quietly become a test of nothing. + VERIFY_IS_TRUE(spans.size() > 0); + for (int span : spans) { + VERIFY_ARE_EQUAL(testCase.expectedSpan, span); + } + } +} + +// Counts stores of the given value into a shadow alloca, i.e. stores whose +// destination is a local pointer rather than a module-scope global. +static uint32_t +CountStoresToAllocaOfValue(std::vector const &disassemblyLines, + const char *value) { + uint32_t count = 0; + for (auto const &line : disassemblyLines) { + if (line.find("store ") == std::string::npos) { + continue; + } + if (line.find(value) == std::string::npos) { + continue; + } + // A store into the original global names the global; the shadow stores this + // pass emits target an alloca reached through a local GEP. + if (line.find('@') != std::string::npos) { + continue; + } + count++; + } + return count; +} + +// A flattened multi-dimensional array member renames the module global but +// not the debug variable, so the pass gathers shadow storage by linkage +// name. Keying it on the debug name instead loses the shadow store for every +// write into the flattened array. +TEST_F(PixTest, PixDbgValueToDbgDeclare_MultiDimensionalStaticGlobalArray) { + const char *source = R"x( +RWByteAddressBuffer RawUAV : register(u0); +struct StaticGlobalHolder +{ + float twoD[2][3]; + float oneD[3]; + float count; +}; +static StaticGlobalHolder g_staticGlobalHolder; +[numthreads(1, 1, 1)] +void main() +{ + g_staticGlobalHolder.oneD[0] = 4.0; + g_staticGlobalHolder.oneD[1] = 5.0; + g_staticGlobalHolder.oneD[2] = 6.0; + g_staticGlobalHolder.twoD[1][0] = 40.0; + g_staticGlobalHolder.twoD[1][2] = 42.0; + g_staticGlobalHolder.count = 1; + + float accumulator = 0; + uint index = 0; + [loop] + while (true) + { + accumulator += g_staticGlobalHolder.twoD[index % 2][index % 3]; + accumulator += g_staticGlobalHolder.oneD[index % 3]; + if (index++ == 4) + { + break; + } + } + RawUAV.Store(64, asuint(accumulator + g_staticGlobalHolder.count)); +})x"; + + auto compiled = Compile(m_dllSupport, source, L"cs_6_0", {L"-Od"}); + CComPtr dxilPart = FindModule(DFCC_ShaderDebugInfoDXIL, compiled); + auto output = RunValueToDeclarePass(dxilPart); + auto lines = Split(Disassemble(output.blob), '\n'); + + // The one-dimensional array in the same struct is the control: it is handled + // correctly whether or not the multi-dimensional case is. + VERIFY_ARE_EQUAL(1u, CountStoresToAllocaOfValue(lines, "4.000000e+00")); + // The two writes into the two-dimensional array are the point of the test. + VERIFY_ARE_EQUAL(1u, CountStoresToAllocaOfValue(lines, "4.000000e+01")); + VERIFY_ARE_EQUAL(1u, CountStoresToAllocaOfValue(lines, "4.200000e+01")); +} + +// Returns the module with the given instructions at the start of the entry +// point's first block. The disassembler prints a label for that block only +// when the block is named, and instructions placed ahead of a label would form +// a block with no terminator, so the injection follows the label when there is +// one and the definition's brace when there is not. +static std::string InjectIntoEntryBlock(const std::string &disassembly, + const std::string &instructions) { + const std::string definition = "define void @main() {"; + const std::string labelledDefinition = definition + "\nentry:"; + const std::string &anchor = + disassembly.find(labelledDefinition) != std::string::npos + ? labelledDefinition + : definition; + return PixTest::ReplaceOnlyOccurrence(disassembly, anchor, + anchor + "\n" + instructions); +} + +// Whether the disassembler labels an entry point's first block depends on +// whether the module kept the block's name, so the injection below pins its +// point against both forms rather than against the one this build produces. +TEST_F(PixTest, EntryBlockInjection_HandlesLabelledAndUnlabelledFirstBlock) { + const std::string instruction = " %injected = alloca float"; + + const std::string labelled = "define void @main() {\nentry:\n ret void\n}\n"; + VERIFY_ARE_EQUAL("define void @main() {\nentry:\n" + instruction + + "\n ret void\n}\n", + InjectIntoEntryBlock(labelled, instruction)); + + const std::string unlabelled = "define void @main() {\n ret void\n}\n"; + VERIFY_ARE_EQUAL("define void @main() {\n" + instruction + + "\n ret void\n}\n", + InjectIntoEntryBlock(unlabelled, instruction)); +} + +// A value nested three GEPs below the alloca needs the annotator to walk the +// whole ancestor chain, not just one level, before it can record the store's +// !pix-alloca-reg-write. DXC's SROA flattens aggregates before this pass +// runs, so this shape does not arise from HLSL; the module is constructed +// directly here. +TEST_F(PixTest, AllocaRegisterWrite_DeepAggregateChainIsAnnotated) { + auto compiled = Compile(m_dllSupport, R"x( +RWByteAddressBuffer RawUAV : register(u0); +[numthreads(1, 1, 1)] +void main() +{ + RawUAV.Store(0, 0); +})x", + L"cs_6_0", {L"-Od"}); + std::string disassembly = Disassemble(compiled); + + // An alloca of a struct nested three levels deep, then a GEP chain that + // descends every level to select a scalar, then a store into it. The store's + // pointer is three GEPs removed from the alloca. + std::string withDeepStore = InjectIntoEntryBlock( + disassembly, + " %deep = alloca { { { float, float } } }\n" + " %deep.l0 = getelementptr { { { float, float } } }, { { { " + "float, float } } }* %deep, i32 0, i32 0\n" + " %deep.l1 = getelementptr { { float, float } }, { { float, " + "float } }* %deep.l0, i32 0, i32 0\n" + " %deep.l2 = getelementptr { float, float }, { float, float " + "}* %deep.l1, i32 0, i32 1\n" + " store float 1.000000e+00, float* %deep.l2\n"); + + std::vector lines = RunAnnotationPassOnText(withDeepStore); + + bool allocaRegistered = false; + bool storeFound = false; + bool storeAnnotated = false; + for (const std::string &line : lines) { + if (line.find("%deep = alloca") != std::string::npos && + line.find("pix-alloca-reg") != std::string::npos) { + allocaRegistered = true; + } + if (line.find("store float 1.000000e+00, float* %deep.l2") != + std::string::npos) { + storeFound = true; + if (line.find("pix-alloca-reg-write") != std::string::npos) { + storeAnnotated = true; + } + } + } + + // The alloca is registered, so the shape reached the pass and the store below + // is the thing under test rather than an artifact of it being skipped. + VERIFY_IS_TRUE(allocaRegistered); + VERIFY_IS_TRUE(storeFound); + // The store three GEPs deep still carries its alloca-register-write. + VERIFY_IS_TRUE(storeAnnotated); +}