Require narrow StorageBuffer{8,16}BitAccess for storage-buffer 8/16-bit access (#9910) - #12759
Draft
nv-slang-bot[bot] wants to merge 1 commit into
Draft
Require narrow StorageBuffer{8,16}BitAccess for storage-buffer 8/16-bit access (#9910)#12759nv-slang-bot[bot] wants to merge 1 commit into
nv-slang-bot[bot] wants to merge 1 commit into
Conversation
…ffers
An 8/16-bit type used through a storage buffer (RWStructuredBuffer, emitted with the
StorageBuffer storage class) was requiring the broad UniformAndStorageBuffer{8,16}BitAccess
SPIR-V capability instead of the narrower StorageBuffer{8,16}BitAccess. The broad capability
requires a Vulkan feature that some devices do not expose even when the narrow one is
available, so such modules were rejected.
Split SpvStorageClassStorageBuffer out of the coalesced Uniform+StorageBuffer arm in both
switches of requireCapabilitiesForType so storage buffers require the narrow capability and
uniform buffers keep the broad one. Add subsumption so a module that uses the same size in
both a uniform and a storage buffer carries only the (subsuming) broad capability and never
under-requires. Also fix SpvInstParent::addInst to set the parent pointer on a section's
first child, which the capability-removal path relies on.
Contributor
|
Automated notice (PR board sync) — do not reply to this comment. Auto-assigned @zangold-nv as shepherd for this Bot PR. FYI for maintainers: committer signal on the changed files is highest for pdeayton-nv among collaborators other than the assignee. They were not auto-requested; a human may optionally add them as a reviewer. |
1 similar comment
Contributor
|
Automated notice (PR board sync) — do not reply to this comment. Auto-assigned @zangold-nv as shepherd for this Bot PR. FYI for maintainers: committer signal on the changed files is highest for pdeayton-nv among collaborators other than the assignee. They were not auto-requested; a human may optionally add them as a reviewer. |
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
An 8-bit type used only through a storage buffer makes Slang emit the broad SPIR-V capability
UniformAndStorageBuffer8BitAccesswhere the narrowerStorageBuffer8BitAccessis sufficient.Consider the reporter's shader:
Compiled with
-target spirv, this emitsOpCapability UniformAndStorageBuffer8BitAccess. Thatcapability requires the Vulkan feature
uniformAndStorageBuffer8BitAccess, which many devices (e.g.the reporter's RTX 3090) do not advertise even though they do expose
storageBuffer8BitAccess. Themodule is therefore rejected although only storage-buffer 8-bit access is actually used. The 16-bit
analog (
UniformAndStorageBuffer16BitAccessvsStorageBuffer16BitAccess) has the identical bug.This is a gap in #8194: for the Uniform/StorageBuffer pair it wired only the broad variants (it did
separately, and correctly, split the PushConstant and Input/Output capabilities) — not a regression.
Proposed solution
The storage class already arrives distinct at the decision site — a
RWStructuredBuffer<uint8_t>iscorrectly
SpvStorageClassStorageBuffer(addressSpaceToStorageClass), a genuine uniform/constantbuffer is
SpvStorageClassUniform. The information to pick the narrow capability was present andsimply discarded by a shared
switcharm.requireCapabilitiesForTypeinsource/slang/slang-emit-spirv.cppcoalescesSpvStorageClassUniformandSpvStorageClassStorageBufferinto onecasein both its switches(the flag-collection switch and the require switch). This change splits
SpvStorageClassStorageBufferinto its own arm that requires the narrow
StorageBuffer{8,16}BitAccess, whileSpvStorageClassUniformkeeps requiring the broad
UniformAndStorageBuffer{8,16}BitAccess. This mirrors theSpvStorageClassPushConstantarm in the same function, which was already correctly split(
StoragePushConstant8/16).The one subtlety is that the same struct type can be used in both a uniform buffer and a storage
buffer in one module, so both arms can fire (in either order). The broad capability is a strict
superset of the narrow one, so the fix maintains a single invariant: the module carries the minimal
capability set covering all uses — never under-requires (which would be invalid SPIR-V), never
carries both the narrow and the broad form. This is enforced with subsumption logic:
(an upgrade), so ordering never leaves both in the module.
Change summary
source/slang/slang-emit-spirv.cpp(requireCapabilitiesForType)SpvStorageClassStorageBufferfromSpvStorageClassUniformin both switches; require narrowStorageBuffer{8,16}BitAccessfor storage buffers, broad for uniform buffers; add subsumption (skip narrow when broad present; upgrade — add broad and remove narrow — from the uniform arm).source/slang/slang-emit-spirv.cpp(requireSPIRVCapability/removeSPIRVCapability)m_capabilityInsts(capability → emittedOpCapabilityinst) and a symmetricremoveSPIRVCapabilityso a subsumed capability can be unlinked from the module.source/slang/slang-emit-spirv.cpp(SpvInstParent::addInst)inst->parentwhen adding a section's first child. It was previously left null (only the subsequent-child path set it), which the capability-removal path's sibling assertion relies on.tests/spirv/capability-uniform-and-storage.slangIN_*) asserts the broad capability; storage buffer (OUT_*) asserts the narrow one and-NOTthe broad; an independent-size mixed case (broad-16 + narrow-8 coexist); and aSHAREDcase binding oneSharedstruct type through both aConstantBufferand aRWStructuredBufferfor 8- and 16-bit (broad subsumes narrow → broad only).Concepts and vocabulary
StorageBuffer8BitAccess(4448) /StorageBuffer16BitAccess(4433) permit 8/16-bit access through theStorageBufferstorage classonly.
UniformAndStorageBuffer8BitAccess(4449) /...16BitAccess(4434) additionally permit8/16-bit access through the
Uniform(constant/UBO) storage class, and thus subsume the narrowform. All four enumerants are already vendored in
external/spirv— no capdef/header change.requireCapabilitiesForType(IRType*, SpvStorageClass). Emitter helper called during pointer-typeemission. Phase 1 builds a
TypeNeedsStorageFlagsmask of which element sizes to search for (basedon which capabilities are still missing for this storage class); phase 2, after walking the type,
requires the corresponding capabilities.
requireSPIRVCapability/ eager emission. Requiring a capability appends itsOpCapabilityinstruction immediately, on first add.
removeSPIRVCapabilityreverses that by unlinking thetracked instruction — safe because
OpCapabilityhas no result id and is referenced by nothing.Process report
Split of the coalesced arm (both switches). The input shape here — the
SpvStorageClassvalue —is correct and principled:
addressSpaceToStorageClassfaithfully maps aRWStructuredBuffertoSpvStorageClassStorageBufferand a uniform/constant buffer toSpvStorageClassUniform, and bothcallers (
kIROp_SPIRVUntypedPtrTypeand thekIROp_PtrTypefamily) pass it through unchanged. Sothe producer is right; the bug was purely that the consumer collapsed two distinct classes into one
caseand hard-coded the broad capability. The fix therefore belongs exactly here, at the consumer,and does not paper over any upstream representation. Without the split, the test's
OUT_UINT8storage-buffer case emits
UniformAndStorageBuffer8BitAccess(reproduced on top-of-tree) instead ofthe correct
StorageBuffer8BitAccess.Subsumption /
removeSPIRVCapability+m_capabilityInsts. These exist to hold the invariant"minimal covering capability set, never both narrow and broad." Consider a module where the same
8-bit-containing struct is bound as both a
ConstantBuffer(→SpvStorageClassUniform) and aRWStructuredBuffer(→SpvStorageClassStorageBuffer).requireCapabilitiesForTyperuns once perpointer-type emission, so both arms fire, and pointer-type emission order is not fixed. Two orderings:
StorageBuffer8BitAccess; then the uniform armadds
UniformAndStorageBuffer8BitAccessand callsremoveSPIRVCapability(StorageBuffer8BitAccess),unlinking the now-subsumed narrow
OpCapability. Result: broad only.guard sees the broad capability already present and adds nothing. Result: broad only.
Either way the module ends with the single broad capability — never both, and never only the narrow
one when a uniform buffer also needs 8-bit (which would be invalid SPIR-V). The
m_capabilityInstsmap is the minimal state needed to reverse an eagerOpCapabilityemission; it ispopulated in the one place capabilities managed by
requireSPIRVCapabilityare emitted, and read onlyby its symmetric
removeSPIRVCapability, so there is a single source of truth. (The unconditionalShadercapability inemitFrontMatteris emitted directly viaemitOpCapabilityand is deliberatelyoutside this bookkeeping — it is never a subsumption target.) This is a new mechanism, not a duplicate
representation: capability set membership stays in
m_capabilities, and the map only records theinstruction to unlink.
SpvInstParent::addInst— first-childparentfix. ExercisingremoveSPIRVCapabilityuncovered alatent bug.
addInstsetsinst->parent = thisonly on the subsequent-child path; when adding asection's first child it set
m_firstChild = m_lastChild = instand returned without settingparent. So the firstOpCapabilityadded to the Capabilities section hadparent == nullptr. Thatfirst capability is whichever one is required first during emission —
emitFrontMatter(which emitsShader) runs late inemitSPIRVModule(afteremitSPIRVAnyCapabilities), so the storage capsrequired while emitting types/entry points populate the section first. The bug then bites two ways:
removing a narrow capability that happens to be the section's first child is a silent no-op via
removeFromParent'sif (!oldParent) return;(leaving both narrow and broad in the module); andremoving a later capability whose previous sibling is the null-parent first child trips
SLANG_ASSERT(pp->parent == oldParent)— a hard crash, which is what the same-size mixed UBO+SSBO casehit. This is a genuine representation bug — the linked-list invariant "every child of a parent has its
parentset" was simply not established for the first child — so it is fixed at the producer(
addInst), not worked around inremoveFromParent. No consumer usedparent == nullptras an"is-first-child" sentinel (the only other reader, the forward-declared-pointer re-add at the end of
emission, reads
spvPtrType->parentand re-adds it, which is now correct for a first child too), andremoveFromParent'sif (!oldParent) return;guard still handles a genuinely unparented instruction.A note on the test's storage-buffer cases not pinning
-profile spirv_1_3. The pre-existing testpinned SPIR-V 1.3, which is why it never caught this bug: at 1.3 a
RWStructuredBufferis emitted withthe legacy
BufferBlock/Uniformencoding (storage classUniform), for which the broad capabilityis genuinely correct. The bug only manifests with the modern
StorageBufferstorage class(SPIR-V 1.4+/default), so the storage-buffer (
OUT_*,SHARED,MIX) cases use the default profile;the uniform (
IN_*) cases keep-profile spirv_1_3since a constant buffer isUniformin bothencodings.
Closes #9910.
🤖 Generated by an automated Slang coworker — may be inaccurate. A human maintainer should verify.