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(