Fix #11861: honor vk::binding on resource-containing struct params#11873
Merged
Conversation
nv-slang-bot Bot
added a commit
that referenced
this pull request
Jul 1, 2026
Address the peer-review nits on PR #11873 (all non-blocking; the struct recursion itself is unchanged): - Expand tests/spirv/vk-binding-entry-point-param-struct.slang with the recursion paths the diff introduces but the original test did not exercise: multi-level inheritance (the base recursion re-enters more than once), a generic struct field (the substituted getType path the comment relies on), a resource field that is not first (the field loop must continue past plain-data fields), and an array-of-struct (the array arm composed with the struct arm). Each asserts the requested Binding/DescriptorSet under -warnings-as-errors 38010. - Document the recursion's termination invariant in isVkBindingCompatibleEntryPointParameterType: value-typed struct cycles and cyclic inheritance are rejected earlier by E39997/E39999, so the recursion descends a finite, acyclic structure and needs no cycle/depth guard. - Note why MemberFilterStyle::Instance (instance-vs-static, not own-vs-inherited) makes the separate findBaseStructType branch necessary, and why static members are excluded. - tests/diagnostics/vk-binding-entry-point-param-struct-no-resource.slang: drop the unnecessary 'non-exhaustive' directive (all diagnostics are matched by annotations; slang-test flags it as an error otherwise).
jkiviluoto-nv
previously approved these changes
Jul 1, 2026
jkiviluoto-nv
left a comment
Contributor
There was a problem hiding this comment.
Validated locally.
…nt params
The pre-layout diagnostic predicate isVkBindingCompatibleEntryPointParameterType
in slang-check-shader.cpp approximates "will this parameter consume a descriptor
slot?" by AST type, but did not recurse into an aggregate struct. A struct that
CONTAINS resources (e.g. struct Resources { Texture2D tex; SamplerState samp; })
therefore returned false, so E38010 ("attribute ignored") was not suppressed --
even though the post-layout binder honors the annotation and places the fields.
Under -warnings-as-errors 38010 this rejected an otherwise-valid shader.
Make the AST predicate a faithful subset of the binder's contract: a struct is
compatible iff at least one of its fields -- transitively, and including fields
inherited from a base struct -- is compatible. Field types are substituted through
the struct's DeclRef so generic fields are recognized, and nested structs are
handled by the recursive call. Thread an ASTBuilder* through the existing array /
ModifiedType recursion for getFields/getType/findBaseStructType.
Stacked on #11870 (fixes #11857 by removing the too-permissive PtrType leaf), so
the struct recursion is a faithful subset by construction: a struct of only
pointers finds no descriptor-consuming leaf and still (correctly) warns E38010.
Tests:
- tests/spirv/vk-binding-entry-point-param-struct.slang: flat, nested, and
inherited struct-of-resources params honored; no E38010 under -warnings-as-errors.
- tests/diagnostics/vk-binding-entry-point-param-struct-no-resource.slang: struct
of only plain data and struct of only a raw pointer both still warn E38010.
Fixes #11861.
Address the peer-review nits on PR #11873 (all non-blocking; the struct recursion itself is unchanged): - Expand tests/spirv/vk-binding-entry-point-param-struct.slang with the recursion paths the diff introduces but the original test did not exercise: multi-level inheritance (the base recursion re-enters more than once), a generic struct field (the substituted getType path the comment relies on), a resource field that is not first (the field loop must continue past plain-data fields), and an array-of-struct (the array arm composed with the struct arm). Each asserts the requested Binding/DescriptorSet under -warnings-as-errors 38010. - Document the recursion's termination invariant in isVkBindingCompatibleEntryPointParameterType: value-typed struct cycles and cyclic inheritance are rejected earlier by E39997/E39999, so the recursion descends a finite, acyclic structure and needs no cycle/depth guard. - Note why MemberFilterStyle::Instance (instance-vs-static, not own-vs-inherited) makes the separate findBaseStructType branch necessary, and why static members are excluded. - tests/diagnostics/vk-binding-entry-point-param-struct-no-resource.slang: drop the unnecessary 'non-exhaustive' directive (all diagnostics are matched by annotations; slang-test flags it as an error otherwise).
The base branch was changed.
5660a26 to
fcdaab4
Compare
The vk::binding entry-point-parameter loop hoisted `auto astBuilder = linkage->getASTBuilder();` at function-body scope, which shadowed a pre-existing nested `astBuilder` declaration later in validateEntryPoint. MSVC's /W4 /WX treats C4456 (declaration hides previous local) as an error, so build-windows-*-cl-x86_64-gpu failed while gcc/clang (no -Wshadow) built clean. Move the declaration into the loop body (leaf scope), which cannot shadow the now-astBuilder-free function scope or the sibling nested blocks, and keeps the call on one line. Verified shadow-free with `-Wshadow`; the fix is behavior- preserving (getASTBuilder() is a cheap accessor).
jkiviluoto-nv
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Since #11712 ("Honor vk::binding on entry point parameters"), a
[[vk::binding(set, binding)]]annotation on an entry-point parameter whose type is a struct that contains resources is honored
by the binder (the fields are placed correctly) but is also diagnosed with warning
E38010("modifier on entry point parameter is unsupported ... will be ignored"). The diagnosticcontradicts the actual, correct behavior, and under
-warnings-as-errors 38010it rejects anotherwise-valid shader.
Consider this shader:
The binder places
resources.texat(binding 2, set 1)andresources.sampat(binding 3, set 1)— exactly as requested — yet
E38010fires onresources, falsely claiming the attribute was ignored.Proposed solution
The regression is a disagreement between two predicates introduced/used by #11712 that must stay in
sync but don't:
isVkBindingCompatibleEntryPointParameterType(
source/slang/slang-check-shader.cpp) runs invalidateEntryPointbefore layout and approximates"will this parameter consume a descriptor slot?" by inspecting the AST type. It recognized the
leaf resource types (and recursed through arrays /
ModifiedType) but did not recurse into anaggregate
struct, so a resource-containing struct returnedfalse→ the warning was not suppressed.findVkBindingEntryPointParameterResourceInfo/hasSupportedVkBindingOnEntryPointParameter,source/slang/slang-parameter-binding.cpp) decidesafter layout by resource kind: it honors
[[vk::binding]]whenever the parameter's computed typelayout accumulates a
DescriptorTableSlotorSubElementRegisterSpace. A struct-of-resourcesaggregates a
DescriptorTableSlotfrom its fields, so the binder honors it.Because the diagnostic approximates by AST type (did not decompose aggregates) while the binder reads the
computed layout (does decompose aggregates), the two diverge for a struct-of-resources: the binder
honors, the diagnostic warns.
The fix makes the AST predicate a faithful subset of the binder's honoring contract: an aggregate
structis compatible iff at least one of its fields — transitively, and including fields inherited froma base struct — is compatible. This is the principled layer for the fix: the diagnostic is an AST-type
approximation (it runs before any layout is available), so the correct repair is to make that
approximation decompose aggregates the same way the binder's layout does, rather than to mask the symptom
at the call site. Field types are substituted through the struct's
DeclRef, so a generic field such asT texwithT = Texture2Dis still recognized; nested structs are handled by the recursive call.Why this stacks on #11870 rather than standing alone. The struct recursion reuses the same leaf
predicate for each field. If that predicate still returned
truefor a raw pointer (uint*) — as it didbefore #11857 was fixed — then
struct { uint* p; }would be wrongly reported as compatible and itsE38010warning suppressed, even though the binder never positions a pointer (a pointer is abuffer-device-address value, not a descriptor slot). #11870 removes that
PtrTypeleaf (the #11857 fix),so building the struct recursion on top of it keeps the predicate a faithful subset by construction: a
struct of only pointers finds no descriptor-consuming leaf and correctly still warns.
Approach considered and deferred: deciding purely from the computed type layout (a single source of
truth shared by diagnostic and binder) would structurally prevent this class of divergence and would also
subsume the sibling issue #11857. It is left as possible future work because the diagnostic runs before
binding layout, so a type layout is not readily available there; consulting it would require computing an
on-demand layout in the checker or moving the diagnostic after layout, which is a larger, higher-risk
change. This PR takes the smaller, low-risk repair that matches the function's existing recursion style.
Change summary
source/slang/slang-check-shader.cppisVkBindingCompatibleEntryPointParameterType: take anASTBuilder*; add aDeclRefType→StructDeclcase that returnstrueif any instance field (viagetFields, with substituted fieldtypes via
getType) is compatible, or if a base struct (viafindBaseStructType) is. Thread theASTBuilder*through the existing array /ModifiedTyperecursion.validateEntryPointnow passeslinkage->getASTBuilder().tests/spirv/vk-binding-entry-point-param-struct.slang(new): struct-of-resources entry-point paramswith
[[vk::binding]], asserting the requestedBinding/DescriptorSetdecorations and — via-warnings-as-errors 38010— that noE38010fires. Covers each distinct traversal the diff adds:flat, nested, single- and multi-level inheritance (the base recursion re-enters — the emitted
%…_multi_base_base_texname confirms two base hops), a generic struct field(
Wrapper<Texture2D<float4>>— the substitutedgetTypepath the comment relies on, which nothinghad exercised), a resource field that is not first (the field loop must continue past plain-data
fields), and an array-of-struct (the array arm composed with the struct arm).
tests/diagnostics/vk-binding-entry-point-param-struct-no-resource.slang(new): a struct of onlyplain-data fields and a struct of only a raw-pointer field with
[[vk::binding]]both still warnE38010(guards against over-broadening the struct recursion — the pointer case is the vk::binding on pointer entry-point parameters is accepted but ignored #11857-classshape that the recursion must not suppress).
Concepts and vocabulary
[[vk::binding]]only for a parameter whoselayout consumes a
DescriptorTableSlot(a plain texture/buffer/sampler) orSubElementRegisterSpace(a whole-space parameter such as a
ParameterBlock); seeisVkBindingEntryPointParameterResourceKind.The AST predicate's leaf cases are the type-level mirror of that set.
isVkBindingCompatibleEntryPointParameterTyperuns invalidateEntryPointbefore any per-target layout exists, so it must reason about AST types;the binder runs after layout and reads
TypeLayout::ResourceInfo. Keeping the former a faithful subsetof the latter is the invariant this PR restores.
Process report
Why the struct-recursion change is necessary and correct (code trace). For
[[vk::binding(2,1)]] uniform Resources resourceswithstruct Resources { Texture2D tex; SamplerState samp; },validateEntryPoint(
slang-check-shader.cpp) computesisVkBindingCompatibleEntryPointParameterType(Resources). Before this change the function had noDeclRefType/StructDeclcase, so it fell through toreturn false; the call site then sawsupportsVkBindingOnParameter == falsewhile the parameter did carry aGLSLBindingAttribute, andemitted
UnhandledModOnEntryPointParameter(E38010). Meanwhile_generateParameterBindings→hasSupportedVkBindingOnEntryPointParameter(
slang-parameter-binding.cpp) found aDescriptorTableSlotinResources's type layout and placedtex/sampat (2,1)/(3,1). After this change the predicate decomposes the struct: it iterates theinstance fields via
getFields(astBuilder, structDeclRef, MemberFilterStyle::Instance), substitutes eachfield type via
getType(astBuilder, fieldDeclRef), and recurses;texis aResourceType→ returnstrue→ the warning is suppressed, matching the binder. A struct of only plain-data fields finds nocompatible field (and no compatible base) and still returns
false, so E38010 still fires for a trulyignored annotation.
Is the input shape correct, or should a producer be fixed? The input shape (a resource-containing
struct entry-point parameter) is a genuine, valid, supported shape — the binder already honors it. The
defect is only that the diagnostic's pre-layout approximation was incomplete, so the fix belongs in that
approximation. No upstream producer emits a malformed type here.
Over-broadening / faithful-subset check. The struct case returns
trueonly when a genuinedescriptor-consuming leaf is found transitively, so the predicate remains a subset of what the binder
honors — it cannot start suppressing the warning for a parameter the binder would ignore. This is why the
PR stacks on #11870: with the
PtrTypeleaf removed (the #11857 fix), a struct whose only field is a rawpointer finds no descriptor-consuming leaf and correctly still warns E38010, which the new negative test
asserts directly. Targets that do not support the feature
(
supportsVkBindingOnEntryPointParameters == false, e.g. CUDA/Metal/C++) are unaffected and still warn.Recursion termination (no cycle/depth guard, and why). The sibling struct-field walks in this file
(
validateVaryingType,collectGenericStructTypeUses) carry visited-set + depth guards, so it is fairto ask why this one does not. The reason is that every cycle that could make the recursion diverge is
rejected by a fatal front-end diagnostic before
validateEntryPointruns this predicate: a value-typedstruct cycle (
struct S { S next; }) exceeds the nesting limit →E39997(kMaxTypeNestingDepth = 128),and a cyclic inheritance graph →
E39999. So the recursion only ever descends a finite, acyclicfield/base structure. A visited-set would therefore be dead code under correct input (it can never fire),
which the methodology counsels against; the termination invariant is documented at the function instead.
The one residual — a pathologically deep non-cyclic inheritance chain — is a pre-existing
unbounded-recursion property shared by other base-walks in the compiler, not introduced by this change,
and inheritance is itself deprecated (
E30816). Checked empirically:struct S { S next; }as auniformentry param exits cleanly withE39997, not a crash.Coordination with #11857 / #11870. #11870 fixes #11857 (the
PtrTypeleaf is too permissive) byremoving that leaf. This PR is stacked on #11870's branch so the struct recursion is faithful by
construction and there is no textual conflict; the two changes are intended to land together.
Fixes #11861.