Skip to content

[HLSL] Add descriptor table metadata parsing #142492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 58 commits into
base: users/joaosaffran/143201
Choose a base branch
from

Conversation

joaosaffran
Copy link
Contributor

  • adds parsing from metadata into dxcontainer binary
  • adds validations as described in the spec
  • adds testing scenarios
    Closes: #126640

if (std::optional<uint32_t> Val = extractMdIntValue(RangeDescriptorNode, 1))
Range.NumDescriptors = *Val;
else
return reportError(Ctx, "Invalid value for Number of Descriptor in Range");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably have a function like reportInvalidTypeError for all of these. Similar to reportValueError but with a message that will differentiate between the two.

Something like "Parameter: ... expected metadata node of type ... but got ..." I am not sure if you can retrieve the type of an MDNode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have addressed this in a separate branch, will send a follow-up PR

@@ -391,14 +567,12 @@ PreservedAnalyses RootSignatureAnalysisPrinter::run(Module &M,
OS << "Definition for '" << F.getName() << "':\n";

// start root signature header
Space++;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't seem like a related change? I guess it is to conform to use explicit idents everywhere instead of incremeting/decrementing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is just to add consistency in the idents


if (Version == 2 &&
Type != llvm::to_underlying(dxbc::DescriptorRangeType::Sampler)) {
switch (Flags) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I find this switch statement really difficult to follow along and verify it is doing what we want.

I am pretty sure it is implementing the logic from here.

Maybe a templated isFlagSet function and an if-else chain might help?

Or a comment describing/linking to the docs to help.

@joaosaffran joaosaffran requested a review from inbelic June 9, 2025 18:28
diff Outdated
@@ -0,0 +1,34 @@
diff --git a/llvm/include/llvm/BinaryFormat/DXContainer.h b/llvm/include/llvm/BinaryFormat/DXContainer.h
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidentally included this file?

@@ -98,6 +98,17 @@ DESCRIPTOR_RANGE_FLAG(16, DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS)
#undef DESCRIPTOR_RANGE_FLAG
#endif // DESCRIPTOR_RANGE_FLAG

// DESCRIPTOR_RANGE(value, name).
#ifdef DESCRIPTOR_RANGE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the other ifdef blocks have a new line after the ifdef.

@alsepkow
Copy link
Contributor

alsepkow commented Jun 9, 2025

return !(RegisterSpace >= 0xFFFFFFF0 && RegisterSpace <= 0xFFFFFFFF);

nit: I suggest naming to constants for improved readability. It is pretty easy to infer in this case but I think its a good habit. Up to you though.


Refers to: llvm/lib/Target/DirectX/DXILRootSignature.cpp:326 in 72ca44b. [](commit_id = 72ca44b, deletion_comment = False)


static bool verifyDescriptorRangeFlag(uint32_t Version, uint32_t Type,
uint32_t Flags) {
bool IsSampler =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool IsSampler =
const bool IsSampler =

@joaosaffran joaosaffran requested a review from alsepkow June 9, 2025 23:45
@alsepkow
Copy link
Contributor

Space--;

Looks like you forgot to remove this?


Refers to: llvm/lib/Target/DirectX/DXILRootSignature.cpp:636 in 90f7403. [](commit_id = 90f7403, deletion_comment = False)

@@ -391,14 +565,12 @@ PreservedAnalyses RootSignatureAnalysisPrinter::run(Module &M,
OS << "Definition for '" << F.getName() << "':\n";

// start root signature header
Space++;
OS << indent(Space) << "Flags: " << format_hex(RS.Flags, 8) << "\n";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that space is now always 0 is it still needed?
I think you can remove the calls that are effectively 'indent(0)'

case llvm::to_underlying(dxbc::RootParameterType::DescriptorTable): {
const mcdxbc::DescriptorTable &Table =
RS.ParametersContainer.getDescriptorTable(Loc);
OS << indent(Space + 2) << "NumRanges: " << Table.Ranges.size() << "\n";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Suggest avoiding 'magic numbers' and instead use named constants. I assume the 2 is intended to be a tab? Could use 'const uint8_t TabInSpaces = 1'.

Copy link
Contributor

@alsepkow alsepkow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor suggestions from me. But the rest looks good.

Header.ParameterType =
llvm::to_underlying(dxbc::RootParameterType::DescriptorTable);

for (unsigned int I = 2; I < DescriptorTableNode->getNumOperands(); I++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We calculate NumOperands earlier, probably best to store that and reuse it here instead of calling getNumOperands each time through the loop.

See https://llvm.org/docs/CodingStandards.html#don-t-evaluate-end-every-time-through-a-loop

Comment on lines 353 to 359
if (Version == 1) {
if (IsSampler) {
return Flags == 0;
}
return Flags ==
llvm::to_underlying(dxbc::DescriptorRangeFlag::DESCRIPTORS_VOLATILE);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make a lot of sense - version 1 doesn't have a flags field, so why are we verifying it? Furthermore, if this is meaningful to do, it doesn't match the spec in https://github.com/llvm/wg-hlsl/blob/main/proposals/0002-root-signature-in-clang.md#all-the-values-should-be-legal, which states that samplers must have the DESCRIPTORS_VOLATILE flag and SRVs/CBVs/UAVs must have the DATA_VOLATILE flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata representation, for descriptor tables, always contains a flag values, so the spec expect me to verify that. I will fix the values though.

Comment on lines 370 to 397
return Flags == 0 ||
isFlagSet(Flags, dxbc::DescriptorRangeFlag::DESCRIPTORS_VOLATILE) ||
isFlagSet(Flags, dxbc::DescriptorRangeFlag::DATA_VOLATILE) ||
isFlagSet(Flags, dxbc::DescriptorRangeFlag::DATA_STATIC) ||
isFlagSet(
Flags,
dxbc::DescriptorRangeFlag::DATA_STATIC_WHILE_SET_AT_EXECUTE) ||
isFlagSet(Flags,
dxbc::DescriptorRangeFlag::
DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS) ||
isFlagSet(Flags, dxbc::DescriptorRangeFlag::DESCRIPTORS_VOLATILE,
dxbc::DescriptorRangeFlag::DATA_VOLATILE) ||
isFlagSet(
Flags, dxbc::DescriptorRangeFlag::DESCRIPTORS_VOLATILE,
dxbc::DescriptorRangeFlag::DATA_STATIC_WHILE_SET_AT_EXECUTE) ||
isFlagSet(Flags,
dxbc::DescriptorRangeFlag::
DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS,
dxbc::DescriptorRangeFlag::DATA_VOLATILE) ||
isFlagSet(Flags,
dxbc::DescriptorRangeFlag::
DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS,
dxbc::DescriptorRangeFlag::DATA_STATIC) ||
isFlagSet(
Flags,
dxbc::DescriptorRangeFlag::
DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS,
dxbc::DescriptorRangeFlag::DATA_STATIC_WHILE_SET_AT_EXECUTE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this extremely hard to follow. I think it'd make things quite a bit simpler if we broke this down based on which DESCRIPTORS_ behaviours are set, and then check the DATA_ bits based on that.

That is, if DESCRIPTORS_VOLATILE is set, then the DATA_ flags can't contain DATA_STATIC, but for DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS or the default, the three DATA_* values are all fine. We'll also need to check that the DATA_* values are mutually exclusive presumably.

This also all becomes a little bit more readable if we (1) use BitmaskEnum.h so we can avoid a bunch of to_underlying calls:

// Earlier in DXContainer.h:
LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();

// Then define DescriptorRangeFlag like so:
enum class DescriptorRangeFlag : uint32_t {

#define DESCRIPTOR_RANGE_FLAG(Num, Val) Val = Num,
#include "DXContainerConstants.def"

  LLVM_MARK_AS_BITMASK_ENUM(DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS)
};

and then (2), define a local alias for dxbc::DescriptorRangeFlag to shorten things up.

Putting it all together, we get something like:

static bool verifyDescriptorRangeFlag(uint32_t Version, uint32_t Type,
                                      uint32_t FlagsVal) {
  using FlagT = dxbc::DescriptorRangeFlag;
  FlagT Flags = FlagT(FlagsVal);

  const bool IsSampler =
      (Type == llvm::to_underlying(dxbc::DescriptorRangeType::Sampler));

  // The data-specific flags are mutually exclusive.
  FlagT DataFlags = FlagT::DATA_VOLATILE | FlagT::DATA_STATIC |
                    FlagT::DATA_STATIC_WHILE_SET_AT_EXECUTE;
  if (popcount(llvm::to_underlying(Flags & DataFlags)) > 1)
    return false;

  // For volatile descriptors, DATA_STATIC is never valid.
  if ((Flags & FlagT::DESCRIPTORS_VOLATILE) == FlagT::DESCRIPTORS_VOLATILE) {
    FlagT Mask = FlagT::DESCRIPTORS_VOLATILE;
    if (!IsSampler) {
      Mask |= FlagT::DATA_VOLATILE;
      Mask |= FlagT::DATA_STATIC_WHILE_SET_AT_EXECUTE;
    }
    return (Flags & ~Mask) == FlagT::NONE;
  }

  // For default (static) or "STATIC_KEEPING_BUFFER_BOUNDS_CHECKS" descriptors,
  // the other data-specific flags may all be set.
  FlagT Mask = FlagT::DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS;
  if (!IsSampler) {
    Mask |= FlagT::DATA_VOLATILE;
    Mask |= FlagT::DATA_STATIC;
    Mask |= FlagT::DATA_STATIC_WHILE_SET_AT_EXECUTE;
  }
  return (Flags & ~Mask) == FlagT::NONE;
}

Note that I don't think any of the current tests check for the exclusivity of DATA_ flags.

; DXC-NEXT: BaseShaderRegister: 1
; DXC-NEXT: RegisterSpace: 10
; DXC-NEXT: OffsetInDescriptorsFromTableStart: 5
; DXC-NEXT: DESCRIPTORS_VOLATILE: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the DESCRIPTORS_VOLATILE and DATA_VOLATILE values here correct? Might want to get this change in after #143201 to avoid conflicts.

@joaosaffran joaosaffran requested review from alsepkow and bogner June 11, 2025 23:27
enum class DescriptorRangeFlag : uint32_t {
#include "DXContainerConstants.def"

LLVM_MARK_AS_BITMASK_ENUM(DESCRIPTORS_STATIC_KEEPING_BUFFER_BOUNDS_CHECKS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review note: the value used in the macro is required to be the highest value

Comment on lines 103 to 109
DESCRIPTOR_RANGE(4, ERROR)
DESCRIPTOR_RANGE(0, SRV)
DESCRIPTOR_RANGE(1, UAV)
DESCRIPTOR_RANGE(2, CBV)
DESCRIPTOR_RANGE(3, Sampler)
DESCRIPTOR_RANGE(0, NONE)
#undef DESCRIPTOR_RANGE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is an artifact of not rebasing onto #143201?

Otherwise, the values here seem very odd.

Comment on lines 359 to 360
if (popcount(llvm::to_underlying(Flags & DataFlags)) == 1)
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct. It seems we would skip further validation.

If Flags = DESCRIPTORS_VOLATILE | DATA_STATIC, then this will return early as valid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this check is needed at all - once we've assured that at most one of these flags is set the logic below should handle the other cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails llvm/test/CodeGen/DirectX/ContainerData/RootSignature-DescriptorTable-AllValidFlagCombinations.ll, because the DATA_* are allowed to be set without setting any DESCRIPTOR_* flag, so for example the metadata below errors with: error: Invalid value for DescriptorFlag: 2, even it is expected to be valid according to the spec: https://github.com/llvm/wg-hlsl/blob/main/proposals/0002-root-signature-in-clang.md#all-the-things-validated-in-sema.

!12 = !{ !"CBV", i32 5, i32 1, i32 7, i32 5, i32 2 }

.Case("UAV", llvm::to_underlying(dxbc::DescriptorRangeType::UAV))
.Case("Sampler",
llvm::to_underlying(dxbc::DescriptorRangeType::Sampler))
.Default(llvm::to_underlying(dxbc::DescriptorRangeType::ERROR));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right:

  1. I don't think we want an "ERROR" value in the DescriptorRangeType enum
  2. Shouldn't we report an error here?

Comment on lines 359 to 360
if (popcount(llvm::to_underlying(Flags & DataFlags)) == 1)
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this check is needed at all - once we've assured that at most one of these flags is set the logic below should handle the other cases.


const bool IsSampler =
(Type == llvm::to_underlying(dxbc::DescriptorRangeType::Sampler));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #142492 (comment) you pointed out that we do want the (simple) validation for version 1, but I don't see that happening any more. Shouldn't we be doing that here?

@joaosaffran joaosaffran changed the base branch from main to users/joaosaffran/143201 June 12, 2025 18:05
@joaosaffran joaosaffran requested review from bogner and inbelic June 12, 2025 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants