From d08306e2e39b254f880c21fb44e598c9edb5d879 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 4 Jun 2026 13:19:50 -0700 Subject: [PATCH 01/45] [SPIR-V] Add SPV_EXT_descriptor_heap + SPV_KHR_untyped_pointers codegen Building off of #8281, this commit adds a native lowering via SPV_EXT_descriptor_heap and SPV_KHR_untyped_pointers. ResourceDescriptorHeap and SamplerDescriptorHeap are lowered to untyped variables decorated with ResourceHeapEXT and SamplerHeapEXT. Each heap access emits OpUntypedAccessChainKHR into a runtime array of the appropriate descriptor type. Buffer-like resources (StructuredBuffer, ByteAddressBuffer, ConstantBuffer, TextureBuffer) use OpTypeBufferEXT and OpBufferPointerEXT; image and sampler resources use OpLoad. Interlocked operations on RWTexture use OpUntypedImageTexelPointerEXT. Requires -fspv-target-env=vulkan1.3. Assisted by AI. --- docs/SPIR-V.rst | 87 +++- .../clang/include/clang/SPIRV/SpirvBuilder.h | 2 +- .../clang/include/clang/SPIRV/SpirvContext.h | 4 + .../include/clang/SPIRV/SpirvInstruction.h | 12 + tools/clang/lib/SPIRV/CapabilityVisitor.cpp | 6 +- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 6 + tools/clang/lib/SPIRV/DeclResultIdMapper.h | 4 +- tools/clang/lib/SPIRV/EmitVisitor.cpp | 1 + tools/clang/lib/SPIRV/LowerTypeVisitor.cpp | 6 + tools/clang/lib/SPIRV/SpirvBuilder.cpp | 3 +- tools/clang/lib/SPIRV/SpirvContext.cpp | 11 + tools/clang/lib/SPIRV/SpirvEmitter.cpp | 458 ++++++++++++++++-- tools/clang/lib/SPIRV/SpirvEmitter.h | 131 +++++ tools/clang/lib/SPIRV/SpirvInstruction.cpp | 8 +- .../resource-heap-ext-texture.hlsl | 64 --- ...scriptorheap.ext.append-consume.error.hlsl | 19 + .../sm6_6.descriptorheap.ext.buffer.hlsl | 57 +++ ...orheap.ext.constant-buffer-assignment.hlsl | 78 +++ ...iptorheap.ext.constant-texture-buffer.hlsl | 51 ++ ....descriptorheap.ext.counter-ops.error.hlsl | 24 + ..._6.descriptorheap.ext.discarded.error.hlsl | 14 + ..._6.descriptorheap.ext.function-params.hlsl | 97 ++++ .../sm6_6.descriptorheap.ext.gather.hlsl | 42 ++ .../sm6_6.descriptorheap.ext.groupshared.hlsl | 35 ++ .../sm6_6.descriptorheap.ext.load-offset.hlsl | 27 ++ .../sm6_6.descriptorheap.ext.mixed-bound.hlsl | 54 +++ .../sm6_6.descriptorheap.ext.nonuniform.hlsl | 30 ++ ...escriptorheap.ext.rwbyteaddressbuffer.hlsl | 21 + ....descriptorheap.ext.rwtexture-atomics.hlsl | 53 ++ ...6_6.descriptorheap.ext.rwtexture-dims.hlsl | 53 ++ ...6.descriptorheap.ext.sample-grad-bias.hlsl | 40 ++ ...descriptorheap.ext.sampler-comparison.hlsl | 30 ++ ...m6_6.descriptorheap.ext.static-global.hlsl | 64 +++ ...ptorheap.ext.structured-buffer-atomic.hlsl | 28 ++ ...sm6_6.descriptorheap.ext.texture-dims.hlsl | 54 +++ .../sm6_6.descriptorheap.ext.texture-ms.hlsl | 33 ++ ...orheap.ext.texture-sampler-assignment.hlsl | 77 +++ .../sm6_6.descriptorheap.ext.texture.hlsl | 34 ++ .../sm6_6.descriptorheap.ext.texturecube.hlsl | 46 ++ ...m6_6.descriptorheap.ext.typed-formats.hlsl | 59 +++ .../unittests/SPIRV/SpirvContextTest.cpp | 3 + 41 files changed, 1818 insertions(+), 108 deletions(-) delete mode 100644 tools/clang/test/CodeGenSPIRV/resource-heap-ext-texture.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.append-consume.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-buffer-assignment.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-texture-buffer.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.discarded.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.function-params.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.groupshared.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.load-offset.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-bound.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwbyteaddressbuffer.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-atomics.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sample-grad-bias.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sampler-comparison.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.structured-buffer-atomic.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-sampler-assignment.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl diff --git a/docs/SPIR-V.rst b/docs/SPIR-V.rst index da5e914e3e..042dbfd416 100644 --- a/docs/SPIR-V.rst +++ b/docs/SPIR-V.rst @@ -336,6 +336,8 @@ Supported extensions * SPV_KHR_float_controls * SPV_NV_shader_subgroup_partitioned * SPV_KHR_quad_control +* SPV_KHR_untyped_pointers +* SPV_EXT_descriptor_heap Vulkan specific attributes -------------------------- @@ -1993,10 +1995,14 @@ responsibility to provide proper numbers and avoid binding overlaps. ResourceDescriptorHeaps & SamplerDescriptorHeaps ------------------------------------------------ -The SPIR-V backend supported SM6.6 resource heaps, using 2 extensions: +By default, the SPIR-V backend supports SM6.6 resource heaps by emulating the +heaps with descriptor-indexing runtime arrays, using 2 extensions: + - `SPV_EXT_descriptor_indexing` - `VK_EXT_mutable_descriptor_type` +This is also the behavior selected by ``-fspv-use-emulated-heap``. + Each type loaded from a heap is considered to be an unbounded RuntimeArray bound to the descriptor set 0. @@ -2074,6 +2080,85 @@ Bindings & sets associated with each heap can be explicitly set using: - `-fvk-bind-counter-heap `: Specify Vulkan binding number and set number for the counter heap. +Native descriptor heap extension lowering +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When ``-fspv-use-descriptor-heap`` is specified, DXC lowers +``ResourceDescriptorHeap`` and ``SamplerDescriptorHeap`` through +``SPV_EXT_descriptor_heap`` instead of the default emulated heap path. This +also requires ``SPV_KHR_untyped_pointers`` and ``-fspv-target-env=vulkan1.3`` +(targeting a lower environment is an error), and a SPIRV-Headers / SPIRV-Tools +build that defines these extensions. The emitted module declares the heap +objects as untyped variables in ``UniformConstant`` storage class: + +.. code:: spirv + + %uptr_uc = OpTypeUntypedPointerKHR UniformConstant + %resource_heap = OpUntypedVariableKHR %uptr_uc UniformConstant + %sampler_heap = OpUntypedVariableKHR %uptr_uc UniformConstant + OpDecorate %resource_heap BuiltIn ResourceHeapEXT + OpDecorate %sampler_heap BuiltIn SamplerHeapEXT + +The concrete descriptor type is selected at each heap access. For image, +sampler, and texel buffer resources, DXC forms a runtime array of that +descriptor type and decorates the array with a byte ``ArrayStride``, then uses +``OpUntypedAccessChainKHR`` followed by ``OpLoad``: + +.. code:: spirv + + %image_type = OpTypeImage %float 2D 2 0 0 1 Unknown + %image_array = OpTypeRuntimeArray %image_type + OpDecorate %image_array ArrayStride 64 + %descriptor = OpUntypedAccessChainKHR %uptr_uc %image_array %resource_heap %index + %image = OpLoad %image_type %descriptor + +For buffer-like resources, DXC uses ``OpTypeBufferEXT`` as the descriptor type +and ``OpBufferPointerEXT`` to recover the pointer to the buffer data. The +descriptor storage class matches the recovered buffer pointer storage class; for +example, ``ConstantBuffer`` uses ``Uniform`` and ``TextureBuffer`` uses +``StorageBuffer``: + +.. code:: spirv + + %buffer_type = OpTypeBufferEXT Uniform + %buffer_array = OpTypeRuntimeArray %buffer_type + OpDecorate %buffer_array ArrayStride 64 + %descriptor = OpUntypedAccessChainKHR %uptr_uc %buffer_array %resource_heap %index + %buffer_ptr = OpBufferPointerEXT %_ptr_Uniform_type_BufferData %descriptor + +For ``RWTexture`` resources loaded from ``ResourceDescriptorHeap``, interlocked +operations that need a texel pointer use ``OpUntypedImageTexelPointerEXT``. +The image descriptor pointer produced by ``OpUntypedAccessChainKHR`` is passed +directly to the texel-pointer instruction instead of first storing the image +handle into a function-scope image variable: + +.. code:: spirv + + %image_type = OpTypeImage %uint 2D 2 0 0 2 R32ui + %image_array = OpTypeRuntimeArray %image_type + %descriptor = OpUntypedAccessChainKHR %uptr_uc %image_array %resource_heap %index + %uptr_image = OpTypeUntypedPointerKHR Image + %texel_ptr = OpUntypedImageTexelPointerEXT %uptr_image %image_type %descriptor %coord %sample + %old = OpAtomicIAdd %uint %texel_ptr %scope %semantics %value + +This path supports texture, RWTexture, sampler, Buffer/RWBuffer, +StructuredBuffer/RWStructuredBuffer without associated counter operations, +ByteAddressBuffer/RWByteAddressBuffer, ConstantBuffer, and TextureBuffer heap +loads, including direct field and array-element accesses for +``ConstantBuffer`` and ``TextureBuffer``. ``NonUniformResourceIndex`` is +accepted but the ``NonUniform`` decoration is not emitted on +``OpUntypedAccessChainKHR`` or the loaded value; ``SPV_EXT_descriptor_heap`` +deprecates the ``NonUniform`` decoration for heap accesses. + +Append/consume structured buffers and UAV counter heap lowering are not +supported by the native descriptor heap path yet. Those forms should continue +to use the default emulated heap lowering, or DXC will emit a diagnostic for +unsupported append/consume structured-buffer heap loads. Heap-loaded +``RWStructuredBuffer`` resources are supported for ordinary data access, but +associated counter operations such as ``IncrementCounter`` and +``DecrementCounter`` emit a diagnostic because the native descriptor heap path +does not recover an associated counter descriptor. + HLSL Expressions ================ diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 2e697a94e0..71c62c427b 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -277,7 +277,7 @@ class SpirvBuilder { /// \brief Creates an OpUntypedImageTexelPointerEXT SPIR-V instruction with /// the given parameters. SpirvUntypedImageTexelPointerEXT *createUntypedImageTexelPointerEXT( - QualType resultType, SpirvInstruction *image, + QualType resultType, const SpirvType *imageType, SpirvInstruction *image, SpirvInstruction *coordinate, SpirvInstruction *sample, SourceLocation); /// \brief Creates an OpConverPtrToU SPIR-V instruction with the given diff --git a/tools/clang/include/clang/SPIRV/SpirvContext.h b/tools/clang/include/clang/SPIRV/SpirvContext.h index 97a62d4af8..8a49f961ca 100644 --- a/tools/clang/include/clang/SPIRV/SpirvContext.h +++ b/tools/clang/include/clang/SPIRV/SpirvContext.h @@ -298,6 +298,7 @@ class SpirvContext { spv::StorageClass); const UntypedPointerKHRType *getUntypedPointerKHRType(spv::StorageClass sc); + const BufferEXTType *getBufferEXTType(spv::StorageClass sc); FunctionType *getFunctionType(const SpirvType *ret, llvm::ArrayRef param); @@ -536,6 +537,9 @@ class SpirvContext { llvm::DenseMap untypedPointerKHRTypes; + llvm::DenseMap + bufferEXTTypes; llvm::MapVector forwardPointerTypes; llvm::MapVector forwardReferences; llvm::DenseSet functionTypes; diff --git a/tools/clang/include/clang/SPIRV/SpirvInstruction.h b/tools/clang/include/clang/SPIRV/SpirvInstruction.h index 39cff251c6..d83d8f7294 100644 --- a/tools/clang/include/clang/SPIRV/SpirvInstruction.h +++ b/tools/clang/include/clang/SPIRV/SpirvInstruction.h @@ -2065,6 +2065,7 @@ class SpirvImageTexelPointer : public SpirvInstruction { class SpirvUntypedImageTexelPointerEXT : public SpirvInstruction { public: SpirvUntypedImageTexelPointerEXT(QualType resultType, SourceLocation loc, + const SpirvType *imageType, SpirvInstruction *image, SpirvInstruction *coordinate, SpirvInstruction *sample); @@ -2078,11 +2079,22 @@ class SpirvUntypedImageTexelPointerEXT : public SpirvInstruction { bool invokeVisitor(Visitor *v) override; + const SpirvType *getImageType() const { return imageType; } SpirvInstruction *getImage() const { return image; } SpirvInstruction *getCoordinate() const { return coordinate; } SpirvInstruction *getSample() const { return sample; } + void replaceOperand( + llvm::function_ref remapOp, + bool inEntryFunctionWrapper) override { + // imageType is a compile-time SpirvType, not an SSA operand. + image = remapOp(image); + coordinate = remapOp(coordinate); + sample = remapOp(sample); + } + private: + const SpirvType *imageType; SpirvInstruction *image; SpirvInstruction *coordinate; SpirvInstruction *sample; diff --git a/tools/clang/lib/SPIRV/CapabilityVisitor.cpp b/tools/clang/lib/SPIRV/CapabilityVisitor.cpp index 9f012d24e8..c48f898a28 100644 --- a/tools/clang/lib/SPIRV/CapabilityVisitor.cpp +++ b/tools/clang/lib/SPIRV/CapabilityVisitor.cpp @@ -955,8 +955,10 @@ bool CapabilityVisitor::visit(SpirvModule *, Visitor::Phase phase) { {spv::Capability::QuadControlKHR}); if (spvOptions.useDescriptorHeap) { - addExtension(Extension::EXT_descriptor_heap, "DescriptorHeap", {}); - addExtension(Extension::KHR_untyped_pointers, "DescriptorHeap", {}); + const llvm::StringRef feature = "DescriptorHeap"; + featureManager.requestTargetEnv(SPV_ENV_VULKAN_1_3, feature, {}); + addExtension(Extension::EXT_descriptor_heap, feature, {}); + addExtension(Extension::KHR_untyped_pointers, feature, {}); addCapability(spv::Capability::DescriptorHeapEXT); } diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 3a88334ffc..f712a10223 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -1136,6 +1136,12 @@ DeclResultIdMapper::createFnVar(const VarDecl *var, return varInstr; } +void DeclResultIdMapper::registerFnVarAlias(const VarDecl *var, + SpirvInstruction *varInstr) { + if (varInstr) + registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); +} + SpirvDebugGlobalVariable *DeclResultIdMapper::createDebugGlobalVariable( SpirvVariable *var, const QualType &type, const SourceLocation &loc, const StringRef &name) { diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.h b/tools/clang/lib/SPIRV/DeclResultIdMapper.h index adfded231e..b7730bb9f3 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.h +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.h @@ -10,7 +10,6 @@ #ifndef LLVM_CLANG_LIB_SPIRV_DECLRESULTIDMAPPER_H #define LLVM_CLANG_LIB_SPIRV_DECLRESULTIDMAPPER_H -#include #include #include "dxc/Support/SPIRVOptions.h" @@ -286,6 +285,9 @@ class DeclResultIdMapper { SpirvVariable *createFnVar(const VarDecl *var, llvm::Optional init); + /// \brief Registers a function-scope alias to an existing instruction. + void registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr); + /// \brief Creates a file-scope variable and returns its instruction. SpirvVariable *createFileVar(const VarDecl *var, llvm::Optional init); diff --git a/tools/clang/lib/SPIRV/EmitVisitor.cpp b/tools/clang/lib/SPIRV/EmitVisitor.cpp index fcdcf5bcd0..75d081ee62 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.cpp +++ b/tools/clang/lib/SPIRV/EmitVisitor.cpp @@ -820,6 +820,7 @@ bool EmitVisitor::visit(SpirvUntypedImageTexelPointerEXT *inst) { initInstruction(inst); curInst.push_back(inst->getResultTypeId()); curInst.push_back(getOrAssignResultId(inst)); + curInst.push_back(typeHandler.emitType(inst->getImageType())); curInst.push_back(getOrAssignResultId(inst->getImage())); curInst.push_back( getOrAssignResultId(inst->getCoordinate())); diff --git a/tools/clang/lib/SPIRV/LowerTypeVisitor.cpp b/tools/clang/lib/SPIRV/LowerTypeVisitor.cpp index b330d40d85..2ac63047c9 100644 --- a/tools/clang/lib/SPIRV/LowerTypeVisitor.cpp +++ b/tools/clang/lib/SPIRV/LowerTypeVisitor.cpp @@ -224,6 +224,12 @@ bool LowerTypeVisitor::visitInstruction(SpirvInstruction *instr) { instr->setResultType(pointerType); break; } + case spv::Op::OpUntypedImageTexelPointerEXT: { + instr->setResultType( + spvContext.getUntypedPointerKHRType(spv::StorageClass::Image)); + instr->setStorageClass(spv::StorageClass::Image); + break; + } // Sparse image operations return a sparse residency struct. case spv::Op::OpImageSparseSampleImplicitLod: case spv::Op::OpImageSparseSampleExplicitLod: diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index d8eceaccc6..68e50b6574 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -533,13 +533,14 @@ SpirvImageTexelPointer *SpirvBuilder::createImageTexelPointer( SpirvUntypedImageTexelPointerEXT * SpirvBuilder::createUntypedImageTexelPointerEXT(QualType resultType, + const SpirvType *imageType, SpirvInstruction *image, SpirvInstruction *coordinate, SpirvInstruction *sample, SourceLocation loc) { assert(insertPoint && "null insert point"); auto *instruction = new (context) SpirvUntypedImageTexelPointerEXT( - resultType, loc, image, coordinate, sample); + resultType, loc, imageType, image, coordinate, sample); insertPoint->addInstruction(instruction); return instruction; } diff --git a/tools/clang/lib/SPIRV/SpirvContext.cpp b/tools/clang/lib/SPIRV/SpirvContext.cpp index c28da78f9d..a3397d74b4 100644 --- a/tools/clang/lib/SPIRV/SpirvContext.cpp +++ b/tools/clang/lib/SPIRV/SpirvContext.cpp @@ -65,6 +65,9 @@ SpirvContext::~SpirvContext() { for (auto *npaType : nodePayloadArrayTypes) npaType->~NodePayloadArrayType(); + for (auto &pair : bufferEXTTypes) + pair.second->~BufferEXTType(); + for (auto *fnType : functionTypes) fnType->~FunctionType(); @@ -402,6 +405,14 @@ const StructType *SpirvContext::getByteAddressBufferType(bool isWritable) { !isWritable, StructInterfaceType::StorageBuffer); } +const BufferEXTType *SpirvContext::getBufferEXTType(spv::StorageClass sc) { + auto found = bufferEXTTypes.find(sc); + if (found != bufferEXTTypes.end()) + return found->second; + + return bufferEXTTypes[sc] = new (this) BufferEXTType(sc); +} + const StructType *SpirvContext::getACSBufferCounterType() { // Create int32. const auto *int32Type = getSIntType(32); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index f05c0d9553..3f46837341 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -1227,6 +1227,12 @@ SpirvInstruction *SpirvEmitter::doExpr(const Expr *expr, auto *decl = declRefExpr->getDecl(); if (isImplicitVarDeclInVkNamespace(declRefExpr->getDecl())) { result = doExpr(cast(decl)->getInit()); + } else if (const auto *varDecl = dyn_cast(decl)) { + if (auto *alias = + emitDescriptorHeapBufferPointer(varDecl, expr->getLocStart())) + result = alias; + else + result = declIdMapper.getDeclEvalInfo(decl, expr->getLocStart(), range); } else { result = declIdMapper.getDeclEvalInfo(decl, expr->getLocStart(), range); } @@ -2012,6 +2018,22 @@ void SpirvEmitter::doEnumDecl(const EnumDecl *decl) { declIdMapper.createEnumConstant(*it); } +bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, + const Expr *init) { + if (!spirvOptions.useDescriptorHeap || !init || + !isDescriptorHeap(init->IgnoreParenCasts())) + return false; + + if (isConstantTextureBuffer(decl->getType()) || + isAKindOfStructuredOrByteBuffer(decl->getType())) { + (void)doExpr(init->IgnoreParenCasts()); + tryToAssignDescriptorHeapBufferAlias(decl, init); + return true; + } + + return false; +} + void SpirvEmitter::doVarDecl(const VarDecl *decl) { if (!validateVKAttributes(decl)) return; @@ -2201,8 +2223,11 @@ void SpirvEmitter::doVarDecl(const VarDecl *decl) { declIdMapper.tryToCreateConstantVar(decl)) return; var = declIdMapper.createFileVar(decl, llvm::None); - } else + } else { + if (tryToCreateDescriptorHeapAlias(decl, decl->getInit())) + return; var = declIdMapper.createFnVar(decl, llvm::None); + } // Emit OpStore to initialize the variable // TODO: revert back to use OpVariable initializer @@ -2225,6 +2250,7 @@ void SpirvEmitter::doVarDecl(const VarDecl *decl) { spvBuilder.createStore(var, constInit, loc, range); } else { storeValue(var, loadIfGLValue(init), decl->getType(), loc, range); + tryToAssignDescriptorHeapImageAlias(decl, init); } // Update counter variable associated with local variables @@ -3115,6 +3141,33 @@ SpirvEmitter::doArraySubscriptExpr(const ArraySubscriptExpr *expr, return loadVal; } +llvm::Optional +SpirvEmitter::tryToAssignToDescriptorHeapBuffer( + const BinaryOperator *assignExpr) { + if (!spirvOptions.useDescriptorHeap) + return llvm::None; + + const QualType lhsType = assignExpr->getLHS()->getType(); + if (!isConstantTextureBuffer(lhsType) && + !isAKindOfStructuredOrByteBuffer(lhsType)) + return llvm::None; + + const Expr *rhsValue = assignExpr->getRHS()->IgnoreParenCasts(); + if (!isDescriptorHeap(rhsValue)) + return llvm::None; + + (void)doExpr(rhsValue); + if (!tryToAssignDescriptorHeapBufferAlias(assignExpr->getLHS(), + assignExpr->getRHS())) + return llvm::None; + + const auto *decl = + dyn_cast_or_null(getReferencedDef(assignExpr->getLHS())); + if (!decl) + return static_cast(nullptr); + return emitDescriptorHeapBufferPointer(decl, assignExpr->getExprLoc()); +} + SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { const auto opcode = expr->getOpcode(); @@ -3124,7 +3177,14 @@ SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { // Update counter variable associated with lhs of assignments tryToAssignCounterVar(expr->getLHS(), expr->getRHS()); - return processAssignment(expr->getLHS(), loadIfGLValue(expr->getRHS()), + if (llvm::Optional aliasResult = + tryToAssignToDescriptorHeapBuffer(expr)) + return aliasResult.getValue(); + + auto *rhs = loadIfGLValue(expr->getRHS()); + tryToAssignDescriptorHeapImageAlias(expr->getLHS(), expr->getRHS()); + + return processAssignment(expr->getLHS(), rhs, /*isCompoundAssignment=*/false, nullptr, expr->getSourceRange()); } @@ -5028,9 +5088,237 @@ SpirvEmitter::processStructuredBufferLoad(const CXXMemberCallExpr *expr) { auto *zero = spvBuilder.getConstantInt(astContext.IntTy, llvm::APInt(32, 0)); auto *index = doExpr(expr->getArg(0)); - return derefOrCreatePointerToValue(buffer->getType(), info, structType, - {zero, index}, buffer->getExprLoc(), - range); + auto *result = + derefOrCreatePointerToValue(buffer->getType(), info, structType, + {zero, index}, buffer->getExprLoc(), range); + + // derefOrCreatePointerToValue returns an lvalue (AccessChain) when the base + // is an lvalue. This covers descriptor-heap buffers reached either directly + // (ResourceDescriptorHeap[i].Load()) or through a local alias var. + // StructuredBuffer::Load semantically returns a value, and the AST emits no + // LValueToRValue cast for the call expression, so emit the load explicitly. + // (Verified required: scoping this to alias vars only regresses the direct + // heap-access tests; non-heap callers are unaffected in the existing suite.) + if (result && !result->isRValue()) { + result = + spvBuilder.createLoad(structType, result, buffer->getExprLoc(), range); + } + + return result; +} + +void SpirvEmitter::markDescriptorHeapCounterUnsupported( + const DeclaratorDecl *decl) { + if (decl) + descriptorHeapUnsupportedCounters.insert(decl); +} + +bool SpirvEmitter::isDescriptorHeapCounterUnsupported(const Expr *expr) const { + if (const auto *decl = getReferencedDef(expr)) + return descriptorHeapUnsupportedCounters.count(decl) != 0; + return false; +} + +SpirvInstruction *SpirvEmitter::emitDescriptorHeapAccessChain( + const SpirvType *arrayType, SpirvInstruction *heap, SpirvVariable *indexVar, + SourceLocation loc) { + const auto *untypedUniformConstantType = + spvContext.getUntypedPointerKHRType(spv::StorageClass::UniformConstant); + auto *index = spvBuilder.createLoad(astContext.UnsignedIntTy, indexVar, loc); + return spvBuilder.createUntypedAccessChainKHR(untypedUniformConstantType, + arrayType, heap, index, loc); +} + +void SpirvEmitter::storeDescriptorHeapIndex(SpirvVariable *indexVar, + SpirvInstruction *index, + QualType indexType, + const Expr *srcExpr) { + if (!astContext.hasSameType(indexType, astContext.UnsignedIntTy)) + index = castToType(index, indexType, astContext.UnsignedIntTy, + srcExpr->getExprLoc(), srcExpr->getSourceRange()); + spvBuilder.createStore(indexVar, index, srcExpr->getExprLoc(), + srcExpr->getSourceRange()); +} + +SpirvVariable * +SpirvEmitter::createDescriptorHeapIndexVar(const VarDecl *dstVar) { + const std::string name = dstVar->getName().str() + ".descriptor.index"; + return spvBuilder.addFnVar(astContext.UnsignedIntTy, dstVar->getLocation(), + name); +} + +bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( + const DeclaratorDecl *dstDecl, const Expr *srcExpr) { + if (!spirvOptions.useDescriptorHeap || !dstDecl || !srcExpr) + return false; + + const auto *dstVar = dyn_cast(dstDecl); + if (!dstVar || + (!isRWTexture(dstVar->getType()) && !isRWBuffer(dstVar->getType()))) + return false; + + const auto *src = srcExpr->IgnoreParenCasts(); + auto found = descriptorHeapImageAccesses.find(src); + if (found == descriptorHeapImageAccesses.end()) + return false; + + auto &alias = descriptorHeapImageAliasVars[dstVar]; + if (!alias.indexVar) + alias.indexVar = createDescriptorHeapIndexVar(dstVar); + alias.imageType = found->second.imageType; + alias.arrayType = found->second.arrayType; + alias.heap = found->second.heap; + storeDescriptorHeapIndex(alias.indexVar, found->second.index, + found->second.indexType, srcExpr); + return true; +} + +bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias(const Expr *dstExpr, + const Expr *srcExpr) { + return tryToAssignDescriptorHeapImageAlias(getReferencedDef(dstExpr), + srcExpr); +} + +bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( + const DeclaratorDecl *dstDecl, const Expr *srcExpr) { + if (!spirvOptions.useDescriptorHeap || !dstDecl || !srcExpr) + return false; + + const auto *dstVar = dyn_cast(dstDecl); + if (!dstVar || !(isConstantTextureBuffer(dstVar->getType()) || + isAKindOfStructuredOrByteBuffer(dstVar->getType()))) + return false; + + const auto *src = srcExpr->IgnoreParenCasts(); + auto found = descriptorHeapBufferAccesses.find(src); + if (found == descriptorHeapBufferAccesses.end()) + return false; + + if (isRWStructuredBuffer(dstVar->getType())) + markDescriptorHeapCounterUnsupported(dstVar); + + auto &alias = descriptorHeapBufferAliasVars[dstVar]; + if (!alias.indexVar) + alias.indexVar = createDescriptorHeapIndexVar(dstVar); + alias.bufferPointerType = found->second.bufferPointerType; + alias.arrayType = found->second.arrayType; + alias.heap = found->second.heap; + alias.layoutRule = found->second.layoutRule; + storeDescriptorHeapIndex(alias.indexVar, found->second.index, + found->second.indexType, srcExpr); + return true; +} + +bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias(const Expr *dstExpr, + const Expr *srcExpr) { + return tryToAssignDescriptorHeapBufferAlias(getReferencedDef(dstExpr), + srcExpr); +} + +SpirvInstruction * +SpirvEmitter::emitDescriptorHeapBufferPointer(const VarDecl *decl, + SourceLocation loc) { + auto found = descriptorHeapBufferAliasVars.find(decl); + if (found == descriptorHeapBufferAliasVars.end()) + return nullptr; + + auto *descriptorPtr = emitDescriptorHeapAccessChain( + found->second.arrayType, found->second.heap, found->second.indexVar, loc); + auto *bufferDataPtr = spvBuilder.createUnaryOp( + spv::Op::OpBufferPointerEXT, found->second.bufferPointerType, + descriptorPtr, loc); + bufferDataPtr->setStorageClass( + found->second.bufferPointerType->getStorageClass()); + bufferDataPtr->setLayoutRule(found->second.layoutRule); + bufferDataPtr->setRValue(false); + return bufferDataPtr; +} + +SpirvInstruction *SpirvEmitter::emitDescriptorHeapImageTexelPointer( + const VarDecl *decl, SpirvInstruction *coordinate, SpirvInstruction *sample, + QualType resultType, SourceLocation loc) { + auto found = descriptorHeapImageAliasVars.find(decl); + if (found == descriptorHeapImageAliasVars.end()) + return nullptr; + + auto *descriptorPtr = emitDescriptorHeapAccessChain( + found->second.arrayType, found->second.heap, found->second.indexVar, loc); + auto *ptr = spvBuilder.createUntypedImageTexelPointerEXT( + resultType, found->second.imageType, descriptorPtr, coordinate, sample, + loc); + ptr->setStorageClass(spv::StorageClass::Image); + return ptr; +} + +// Descriptor-heap buffers: ConstantBuffer is a UBO (Uniform); every other +// buffer resource (Structured/RW/ByteAddress, TextureBuffer) is an SSBO +// (StorageBuffer). Here because the opaque OpTypeBufferEXT descriptor +// carries no pointee interface type, so RemoveBufferBlockVisitor +// cannot infer/correct its storage class post-lowering. +static spv::StorageClass +getDescriptorHeapBufferStorageClass(QualType resourceType) { + return isConstantBuffer(resourceType) ? spv::StorageClass::Uniform + : spv::StorageClass::StorageBuffer; +} + +SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( + QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, + const Expr *expr, const Expr *baseExpr, const Expr *indexExpr) { + const auto *untypedUniformConstantType = + spvContext.getUntypedPointerKHRType(spv::StorageClass::UniformConstant); + LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, + spvBuilder); + const SpirvType *bufferDataType = lowerTypeVisitor.lowerType( + resourceType, SpirvLayoutRule::Void, llvm::None, baseExpr->getExprLoc()); + + const SpirvPointerType *bufferDataPointerType = nullptr; + SpirvLayoutRule layoutRule = spirvOptions.sBufferLayoutRule; + if (isConstantTextureBuffer(resourceType)) { + layoutRule = isConstantBuffer(resourceType) + ? spirvOptions.cBufferLayoutRule + : spirvOptions.tBufferLayoutRule; + bufferDataPointerType = spvContext.getPointerType( + bufferDataType, getDescriptorHeapBufferStorageClass(resourceType)); + } else { + bufferDataPointerType = dyn_cast(bufferDataType); + } + + if (!bufferDataPointerType) { + emitError("descriptor heap buffer type lowering failed", + expr->getExprLoc()); + return nullptr; + } + + // ConstantBuffer -> Uniform (UBO); all others -> StorageBuffer (SSBO) + // TODO: Remove this manual override once LowerTypeVisitor returns the + // correct StorageClass for descriptor-heap alias pointer types + // (currently it returns Uniform for all of them). + const spv::StorageClass bufferExtSC = isConstantBuffer(resourceType) + ? spv::StorageClass::Uniform + : spv::StorageClass::StorageBuffer; + const auto *bufferDescriptorType = spvContext.getBufferEXTType(bufferExtSC); + // Buffer descriptors are always on the resource heap. + const auto *arrayType = getDescriptorHeapRuntimeArrayType( + bufferDescriptorType, /*onSamplerHeap=*/false); + auto *untypedAccessChainPtr = spvBuilder.createUntypedAccessChainKHR( + untypedUniformConstantType, arrayType, heapVar, index, + baseExpr->getExprLoc()); + auto *bufferDataPtr = spvBuilder.createUnaryOp( + spv::Op::OpBufferPointerEXT, bufferDataPointerType, untypedAccessChainPtr, + baseExpr->getExprLoc()); + bufferDataPtr->setStorageClass(bufferDataPointerType->getStorageClass()); + bufferDataPtr->setLayoutRule(layoutRule); + bufferDataPtr->setRValue(false); + if (isRasterizerOrderedView(resourceType)) { + bufferDataPtr->setRasterizerOrdered(true); + spvBuilder.addExecutionMode(entryFunction, + declIdMapper.getInterlockExecutionMode(), {}, + baseExpr->getExprLoc()); + } + descriptorHeapBufferAccesses[expr] = { + bufferDataPointerType, arrayType, heapVar, index, + indexExpr->getType(), layoutRule}; + return bufferDataPtr; } SpirvInstruction * @@ -5054,6 +5342,22 @@ SpirvEmitter::incDecRWACSBufferCounter(const CXXMemberCallExpr *expr, (void)doExpr(object); } + if (isDescriptorHeapCounterUnsupported(object)) { + emitError("counter operations on heap-loaded RWStructuredBuffer are not " + "supported with SPV_EXT_descriptor_heap", + expr->getCallee()->getExprLoc()); + return nullptr; + } + + if (spirvOptions.useDescriptorHeap && + (isAppendStructuredBuffer(object->getType()) || + isConsumeStructuredBuffer(object->getType()))) { + emitError("append/consume structured buffers are not supported with " + "SPV_EXT_descriptor_heap", + expr->getCallee()->getExprLoc()); + return nullptr; + } + auto *counter = getFinalACSBufferCounterInstruction(object); if (!counter) { emitFatalError("Cannot access associated counter variable for an array of " @@ -5100,6 +5404,11 @@ bool SpirvEmitter::tryToAssignCounterVar(const DeclaratorDecl *dstDecl, declIdMapper.getOrCreateCounterIdAliasPair(dstDecl)) { auto *srcCounter = getFinalACSBufferCounterInstruction(srcExpr); if (!srcCounter) { + if (spirvOptions.useDescriptorHeap && + isDescriptorHeap(srcExpr->IgnoreParenCasts())) { + markDescriptorHeapCounterUnsupported(dstDecl); + return true; + } emitFatalError("cannot find the associated counter variable", srcExpr->getExprLoc()); return false; @@ -5139,6 +5448,11 @@ bool SpirvEmitter::tryToAssignCounterVar(const Expr *dstExpr, auto *srcCounter = getFinalACSBufferCounterInstruction(srcExpr); if ((dstCounter == nullptr) != (srcCounter == nullptr)) { + if (spirvOptions.useDescriptorHeap && dstCounter && + isDescriptorHeap(srcExpr->IgnoreParenCasts())) { + markDescriptorHeapCounterUnsupported(getReferencedDef(dstExpr)); + return true; + } emitFatalError("cannot handle associated counter variable assignment", srcExpr->getExprLoc()); return false; @@ -5270,6 +5584,8 @@ SpirvEmitter::processACSBufferAppendConsume(const CXXMemberCallExpr *expr) { expr, isAppend, // We have already translated the object in the above. Avoid duplication. /*loadObject=*/false); + if (!index) + return nullptr; auto bufferElemTy = hlsl::GetHLSLResourceResultType(object->getType()); @@ -5692,16 +6008,16 @@ SpirvEmitter::processIntrinsicMemberCall(const CXXMemberCallExpr *expr, retVal = processTextureLevelOfDetail(expr, /* unclamped */ true); break; case IntrinsicOp::MOP_IncrementCounter: - retVal = spvBuilder.createUnaryOp( - spv::Op::OpBitcast, astContext.UnsignedIntTy, - incDecRWACSBufferCounter(expr, /*isInc*/ true), - expr->getCallee()->getExprLoc(), expr->getCallee()->getSourceRange()); + if (auto *counter = incDecRWACSBufferCounter(expr, /*isInc*/ true)) + retVal = spvBuilder.createUnaryOp( + spv::Op::OpBitcast, astContext.UnsignedIntTy, counter, + expr->getCallee()->getExprLoc(), expr->getCallee()->getSourceRange()); break; case IntrinsicOp::MOP_DecrementCounter: - retVal = spvBuilder.createUnaryOp( - spv::Op::OpBitcast, astContext.UnsignedIntTy, - incDecRWACSBufferCounter(expr, /*isInc*/ false), - expr->getCallee()->getExprLoc(), expr->getCallee()->getSourceRange()); + if (auto *counter = incDecRWACSBufferCounter(expr, /*isInc*/ false)) + retVal = spvBuilder.createUnaryOp( + spv::Op::OpBitcast, astContext.UnsignedIntTy, counter, + expr->getCallee()->getExprLoc(), expr->getCallee()->getSourceRange()); break; case IntrinsicOp::MOP_Append: if (hlsl::IsHLSLStreamOutputType( @@ -6644,10 +6960,32 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, const Expr *indexExpr = nullptr; getDescriptorHeapOperands(expr, &baseExpr, &indexExpr); - const Expr *parentExpr = cast(parentMap->getParent(expr)); + // The heap index expression must be immediately converted to a concrete + // resource type (an implicit cast inserted by the front-end). If the + // parent is missing or is not a cast (e.g. the result is discarded as + // a statement, or used in a context with no target resource type) we + // cannot determine the resource type. + const auto *parentExpr = + dyn_cast_or_null(parentMap->getParent(expr)); + if (!parentExpr) { + emitError("ResourceDescriptorHeap/SamplerDescriptorHeap indexing must " + "be used as a resource", + expr->getExprLoc()); + return nullptr; + } QualType resourceType = parentExpr->getType(); + // The heap object must be a direct reference to the builtin heap + // variable. Anything else (e.g. a non-variable expression) has no backing + // VarDecl. const auto *declRefExpr = dyn_cast(baseExpr->IgnoreCasts()); - auto *decl = cast(declRefExpr->getDecl()); + const auto *decl = + declRefExpr ? dyn_cast(declRefExpr->getDecl()) : nullptr; + if (!decl) { + emitError("unsupported ResourceDescriptorHeap/SamplerDescriptorHeap " + "expression", + baseExpr->getExprLoc()); + return nullptr; + } auto *var = declIdMapper.createResourceHeap(decl, resourceType); if (hlsl::HasHLSLGloballyCoherent(resourceType)) @@ -6655,27 +6993,44 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, auto *index = doExpr(indexExpr); if (spirvOptions.useDescriptorHeap) { - emitWarning("SPV_EXT_descriptor_heap support is incomplete.", - baseExpr->getExprLoc()); needsLegalization = true; - if (isAKindOfStructuredOrByteBuffer(resourceType)) { - emitError("UAV support not implemented with non-emulated heaps.", + if (isAppendStructuredBuffer(resourceType) || + isConsumeStructuredBuffer(resourceType)) { + emitError("append/consume structured buffers are not supported with " + "SPV_EXT_descriptor_heap", expr->getExprLoc()); return nullptr; } - const auto *untypedType = spvContext.getUntypedPointerKHRType( - spv::StorageClass::UniformConstant); + if (isAKindOfStructuredOrByteBuffer(resourceType) || + isConstantTextureBuffer(resourceType)) { + return emitDescriptorHeapBufferAccess(resourceType, var, index, expr, + baseExpr, indexExpr); + } + + const auto *untypedUniformConstantType = + spvContext.getUntypedPointerKHRType( + spv::StorageClass::UniformConstant); LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, spvBuilder); const SpirvType *handleType = lowerTypeVisitor.lowerType(resourceType, SpirvLayoutRule::Void, llvm::None, baseExpr->getExprLoc()); - const auto *arrayType = - spvContext.getRuntimeArrayType(handleType, llvm::None); + // Images/samplers may come from either heap; pick the right stride. + const auto *arrayType = getDescriptorHeapRuntimeArrayType( + handleType, isSamplerDescriptorHeap(decl)); auto *untypedAccessChainPtr = spvBuilder.createUntypedAccessChainKHR( - untypedType, arrayType, var, index, baseExpr->getExprLoc()); + untypedUniformConstantType, arrayType, var, index, + baseExpr->getExprLoc()); + if (isRasterizerOrderedView(resourceType)) { + spvBuilder.addExecutionMode(entryFunction, + declIdMapper.getInterlockExecutionMode(), + {}, baseExpr->getExprLoc()); + } + descriptorHeapImageAccesses[expr] = { + untypedAccessChainPtr, handleType, arrayType, var, index, + indexExpr->getType()}; return spvBuilder.createLoad(resourceType, untypedAccessChainPtr, baseExpr->getExprLoc(), range); } @@ -8870,6 +9225,16 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { declIdMapper.registerSpecConstant(varDecl, specConstant); } +const SpirvType * +SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType, + bool onSamplerHeap) { + constexpr uint32_t kDefaultResourceHeapStride = 64; + constexpr uint32_t kDefaultSamplerHeapStride = 32; + const uint32_t stride = + onSamplerHeap ? kDefaultSamplerHeapStride : kDefaultResourceHeapStride; + return spvContext.getRuntimeArrayType(elemType, stride); +} + SpirvInstruction * SpirvEmitter::processMatrixBinaryOp(const Expr *lhs, const Expr *rhs, const BinaryOperatorKind opcode, @@ -10569,18 +10934,41 @@ SpirvEmitter::processIntrinsicInterlockedMethod(const CallExpr *expr, return nullptr; } } - auto *baseInstr = doExpr(base); - if (baseInstr->isRValue()) { - // OpImageTexelPointer's Image argument must have a type of - // OpTypePointer with Type OpTypeImage. Need to create a temporary - // variable if the baseId is an rvalue. - baseInstr = - createTemporaryVar(base->getType(), getAstTypeName(base->getType()), - baseInstr, base->getExprLoc()); - } auto *coordInstr = doExpr(index); - ptr = spvBuilder.createImageTexelPointer(baseType, baseInstr, coordInstr, - zero, srcLoc); + + if (spirvOptions.useDescriptorHeap) { + const Expr *heapBase = base->IgnoreParenCasts(); + auto access = descriptorHeapImageAccesses.find(heapBase); + if (access == descriptorHeapImageAccesses.end() && + isDescriptorHeap(heapBase)) { + (void)doExpr(heapBase); + access = descriptorHeapImageAccesses.find(heapBase); + } + if (access != descriptorHeapImageAccesses.end()) { + ptr = spvBuilder.createUntypedImageTexelPointerEXT( + baseType, access->second.imageType, access->second.accessChain, + coordInstr, zero, srcLoc); + ptr->setStorageClass(spv::StorageClass::Image); + } else if (const auto *decl = + dyn_cast_or_null(getReferencedDef(base))) { + ptr = emitDescriptorHeapImageTexelPointer(decl, coordInstr, zero, + baseType, srcLoc); + } + } + + if (!ptr) { + auto *baseInstr = doExpr(base); + if (baseInstr->isRValue()) { + // OpImageTexelPointer's Image argument must have a type of + // OpTypePointer with Type OpTypeImage. Need to create a temporary + // variable if the baseId is an rvalue. + baseInstr = createTemporaryVar(base->getType(), + getAstTypeName(base->getType()), + baseInstr, base->getExprLoc()); + } + ptr = spvBuilder.createImageTexelPointer(baseType, baseInstr, + coordInstr, zero, srcLoc); + } } } if (!ptr) { diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 10cc31023c..76c174eab4 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -33,6 +33,7 @@ #include "clang/SPIRV/FeatureManager.h" #include "clang/SPIRV/SpirvBuilder.h" #include "clang/SPIRV/SpirvContext.h" +#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "ConstEvaluator.h" @@ -398,6 +399,23 @@ class SpirvEmitter : public ASTConsumer { /// Translates the given varDecl into a spec constant. void createSpecConstant(const VarDecl *varDecl); + /// Returns the OpTypeRuntimeArray for a descriptor-heap array of elemType + /// decorated with the default ArrayStride (64 bytes for the resource heap, + /// 32 bytes for the sampler heap). + const SpirvType *getDescriptorHeapRuntimeArrayType(const SpirvType *elemType, + bool onSamplerHeap); + + /// Emits the native (SPV_EXT_descriptor_heap) access for a buffer-like + /// resource (StructuredBuffer/ByteAddressBuffer/ConstantBuffer/TextureBuffer + /// and their RW variants) loaded from heapVar at index: + /// OpUntypedAccessChainKHR -> OpBufferPointerEXT. Records the access in + /// descriptorHeapBufferAccesses[expr] and returns the buffer-data pointer, or + /// nullptr (after emitting an error) on type-lowering failure. Caller must + /// have already checked the resource is buffer-like. + SpirvInstruction *emitDescriptorHeapBufferAccess( + QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, + const Expr *expr, const Expr *baseExpr, const Expr *indexExpr); + /// Generates the necessary instructions for conducting the given binary /// operation on lhs and rhs. /// @@ -1202,6 +1220,74 @@ class SpirvEmitter : public ASTConsumer { const Expr *srcExpr); bool tryToAssignCounterVar(const Expr *dstExpr, const Expr *srcExpr); + /// \brief Marks an alias resource as heap-loaded with no associated counter. + void markDescriptorHeapCounterUnsupported(const DeclaratorDecl *decl); + + /// \brief Returns true if counter operations on the resource expression are + /// known to be unsupported because the resource came from + /// ResourceDescriptorHeap. + bool isDescriptorHeapCounterUnsupported(const Expr *expr) const; + + /// \brief Records the descriptor heap index assigned to a local image + /// resource alias, if the source expression came directly from a descriptor + /// heap. This mirrors the normal resource handle store while preserving + /// enough information to recreate OpUntypedImageTexelPointerEXT after + /// reassignment. + bool tryToAssignDescriptorHeapImageAlias(const DeclaratorDecl *dstDecl, + const Expr *srcExpr); + bool tryToAssignDescriptorHeapImageAlias(const Expr *dstExpr, + const Expr *srcExpr); + bool tryToAssignDescriptorHeapBufferAlias(const DeclaratorDecl *dstDecl, + const Expr *srcExpr); + bool tryToAssignDescriptorHeapBufferAlias(const Expr *dstExpr, + const Expr *srcExpr); + + /// \brief Creates the ".descriptor.index" function variable used to + /// remember the descriptor heap index of a local resource alias dstVar. + SpirvVariable *createDescriptorHeapIndexVar(const VarDecl *dstVar); + + /// \brief If decl is a function-local variable initialized directly from a + /// descriptor heap subscript (e.g. ResourceDescriptorHeap[i]), creates the + /// appropriate alias and returns true. Returns false if decl is not such a + /// descriptor-heap alias and should be emitted as a normal variable. + bool tryToCreateDescriptorHeapAlias(const VarDecl *decl, const Expr *init); + + /// \brief Handles a buffer = ResourceDescriptorHeap[i] assignment. Returns + /// None if assignExpr is not such an assignment (caller should fall back to a + /// normal assignment). Otherwise the alias was created and the wrapped value + /// is the result of the assignment expression (possibly nullptr). + llvm::Optional + tryToAssignToDescriptorHeapBuffer(const BinaryOperator *assignExpr); + + /// \brief Emits the instructions that re-derive the buffer-data pointer for a + /// descriptor-heap buffer alias decl (OpLoad of the saved index, then + /// OpUntypedAccessChainKHR + OpBufferPointerEXT). Returns nullptr if decl + /// is not a recorded heap buffer alias. Not a pure lookup -- it emits. + SpirvInstruction *emitDescriptorHeapBufferPointer(const VarDecl *decl, + SourceLocation loc); + + /// \brief Emits an OpUntypedImageTexelPointerEXT for a descriptor-heap image + /// alias decl (OpLoad of the saved index, then OpUntypedAccessChainKHR + /// feeding the texel pointer). Returns nullptr if decl is not a recorded heap + /// image alias. Symmetric with emitDescriptorHeapBufferPointer. + SpirvInstruction *emitDescriptorHeapImageTexelPointer( + const VarDecl *decl, SpirvInstruction *coordinate, + SpirvInstruction *sample, QualType resultType, SourceLocation loc); + + /// \brief Emits OpLoad of indexVar then OpUntypedAccessChainKHR into the + /// heap, yielding the per-descriptor pointer shared by the buffer/image alias + /// re-derivation paths above. + SpirvInstruction *emitDescriptorHeapAccessChain(const SpirvType *arrayType, + SpirvInstruction *heap, + SpirvVariable *indexVar, + SourceLocation loc); + + /// \brief Stores index (cast to uint when needed) into the alias indexVar, + /// shared by the image/buffer alias-assignment paths. + void storeDescriptorHeapIndex(SpirvVariable *indexVar, + SpirvInstruction *index, QualType indexType, + const Expr *srcExpr); + /// Returns an instruction that points to the alias counter variable with the /// entity represented by expr. /// @@ -1555,6 +1641,51 @@ class SpirvEmitter : public ASTConsumer { /// The SPIR-V function parameter for the current this object. SpirvInstruction *curThis; + /// Native descriptor heap image descriptors used to directly form image + /// atomics. The emitter is single-use per translation unit, so these + /// AST-pointer maps live for the emitter lifetime. + struct DescriptorHeapImageAccess { + SpirvInstruction *accessChain; + const SpirvType *imageType; + const SpirvType *arrayType; + SpirvInstruction *heap; + SpirvInstruction *index; + QualType indexType; + }; + struct DescriptorHeapImageAlias { + SpirvVariable *indexVar; + const SpirvType *imageType; + const SpirvType *arrayType; + SpirvInstruction *heap; + }; + struct DescriptorHeapBufferAccess { + const SpirvPointerType *bufferPointerType; + const SpirvType *arrayType; + SpirvInstruction *heap; + SpirvInstruction *index; + QualType indexType; + SpirvLayoutRule layoutRule; + }; + struct DescriptorHeapBufferAlias { + SpirvVariable *indexVar; + const SpirvPointerType *bufferPointerType; + const SpirvType *arrayType; + SpirvInstruction *heap; + SpirvLayoutRule layoutRule; + }; + llvm::DenseMap + descriptorHeapImageAccesses; + llvm::DenseMap + descriptorHeapImageAliasVars; + llvm::DenseMap + descriptorHeapBufferAccesses; + llvm::DenseMap + descriptorHeapBufferAliasVars; + + /// RWStructuredBuffer aliases loaded from ResourceDescriptorHeap have no + /// associated UAV counter descriptor in the native descriptor heap path. + llvm::DenseSet descriptorHeapUnsupportedCounters; + /// The source location of a push constant block we have previously seen. /// Invalid means no push constant blocks defined thus far. SourceLocation seenPushConstantAt; diff --git a/tools/clang/lib/SPIRV/SpirvInstruction.cpp b/tools/clang/lib/SPIRV/SpirvInstruction.cpp index 399f0fa11f..28c3cfee17 100644 --- a/tools/clang/lib/SPIRV/SpirvInstruction.cpp +++ b/tools/clang/lib/SPIRV/SpirvInstruction.cpp @@ -969,11 +969,13 @@ SpirvImageTexelPointer::SpirvImageTexelPointer(QualType resultType, image(imageInst), coordinate(coordinateInst), sample(sampleInst) {} SpirvUntypedImageTexelPointerEXT::SpirvUntypedImageTexelPointerEXT( - QualType resultType, SourceLocation loc, SpirvInstruction *imageInst, - SpirvInstruction *coordinateInst, SpirvInstruction *sampleInst) + QualType resultType, SourceLocation loc, const SpirvType *spvImageType, + SpirvInstruction *imageInst, SpirvInstruction *coordinateInst, + SpirvInstruction *sampleInst) : SpirvInstruction(IK_UntypedImageTexelPointerEXT, spv::Op::OpUntypedImageTexelPointerEXT, resultType, loc), - image(imageInst), coordinate(coordinateInst), sample(sampleInst) {} + imageType(spvImageType), image(imageInst), coordinate(coordinateInst), + sample(sampleInst) {} SpirvLoad::SpirvLoad(QualType resultType, SourceLocation loc, SpirvInstruction *pointerInst, SourceRange range, diff --git a/tools/clang/test/CodeGenSPIRV/resource-heap-ext-texture.hlsl b/tools/clang/test/CodeGenSPIRV/resource-heap-ext-texture.hlsl deleted file mode 100644 index a6d6f29ea2..0000000000 --- a/tools/clang/test/CodeGenSPIRV/resource-heap-ext-texture.hlsl +++ /dev/null @@ -1,64 +0,0 @@ -// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -spirv %s | FileCheck %s - -// The validator now requires explicit layout for UniformConstant when -// SPV_EXT_descriptor_heap is used (KhronosGroup/SPIRV-Tools#6792). DXC's -// descriptor heap support does not yet emit ArrayStride. -// XFAIL: * - -// CHECK: OpCapability DescriptorHeapEXT -// CHECK: OpExtension "SPV_EXT_descriptor_heap" - -// CHECK-DAG: OpDecorate %[[ResourceHeap:[a-zA-Z0-9_]+]] BuiltIn ResourceHeapEXT -// CHECK-DAG: OpDecorate %[[SamplerHeap:[a-zA-Z0-9_]+]] BuiltIn SamplerHeapEXT - -// CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant -// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown -// CHECK-DAG: %[[RWTex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 2 Rgba32f -// CHECK-DAG: %[[BufferType:[a-zA-Z0-9_]+]] = OpTypeImage %float Buffer 2 0 0 1 Rgba32f -// CHECK-DAG: %[[RWBufferType:[a-zA-Z0-9_]+]] = OpTypeImage %float Buffer 2 0 0 2 Rgba32f -// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler - -// CHECK-DAG: %[[RA_Tex2DType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]] -// CHECK-DAG: %[[RA_RWTex2DType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTex2DType]] -// CHECK-DAG: %[[RA_BufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[BufferType]] -// CHECK-DAG: %[[RA_RWBufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWBufferType]] -// CHECK-DAG: %[[RA_SamplerType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]] - -// CHECK: %[[ResourceHeap]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant -// CHECK: %[[SamplerHeap]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant - -[numthreads(1, 1, 1)] -void main(uint3 tid : SV_DispatchThreadID) { - Texture2D myTex = ResourceDescriptorHeap[0]; - // CHECK: %[[TexIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_Tex2DType]] %[[ResourceHeap]] %uint_0 - // CHECK: %[[TexHandle:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] %[[TexIndex]] - - RWTexture2D myRWTex = ResourceDescriptorHeap[1]; - // CHECK: %[[RWTexIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_RWTex2DType]] %[[ResourceHeap]] %uint_1 - // CHECK: %[[RWTexHandle:[a-zA-Z0-9_]+]] = OpLoad %[[RWTex2DType]] %[[RWTexIndex]] - - Buffer myBuf = ResourceDescriptorHeap[2]; - // CHECK: %[[BufIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_BufferType]] %[[ResourceHeap]] %uint_2 - // CHECK: %[[BufHandle:[a-zA-Z0-9_]+]] = OpLoad %[[BufferType]] %[[BufIndex]] - - RWBuffer myRWBuf = ResourceDescriptorHeap[3]; - // CHECK: %[[RWBufIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_RWBufferType]] %[[ResourceHeap]] %uint_3 - // CHECK: %[[RWBufHandle:[a-zA-Z0-9_]+]] = OpLoad %[[RWBufferType]] %[[RWBufIndex]] - - SamplerState mySamp = SamplerDescriptorHeap[0]; - // CHECK: %[[SampIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_SamplerType]] %[[SamplerHeap]] %uint_0 - // CHECK: %[[SampHandle:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampIndex]] - - // CHECK: %[[SampledImage:[a-zA-Z0-9_]+]] = OpSampledImage %{{.*}} %[[TexHandle]] %[[SampHandle]] - // CHECK: %[[TexResult:[a-zA-Z0-9_]+]] = OpImageSampleExplicitLod %v4float %[[SampledImage]] - float4 texVal = myTex.SampleLevel(mySamp, float2(0, 0), 0); - - // CHECK: %[[BufResult:[a-zA-Z0-9_]+]] = OpImageFetch %v4float %[[BufHandle]] - float4 bufVal = myBuf.Load(tid.x); - - // CHECK: OpImageWrite %[[RWTexHandle]] - myRWTex[tid.xy] = texVal; - - // CHECK: OpImageWrite %[[RWBufHandle]] - myRWBuf[tid.x] = bufVal; -} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.append-consume.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.append-consume.error.hlsl new file mode 100644 index 0000000000..89bf18acaf --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.append-consume.error.hlsl @@ -0,0 +1,19 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_APPEND %s 2>&1 | FileCheck --check-prefix=APPEND %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck --check-prefix=CONSUME %s + +// Verifies: Append/Consume structured buffers via ResourceDescriptorHeap +// emit a hard error under SPV_EXT_descriptor_heap. + +// APPEND: append/consume structured buffers are not supported with SPV_EXT_descriptor_heap +// CONSUME: append/consume structured buffers are not supported with SPV_EXT_descriptor_heap + +[numthreads(1, 1, 1)] +void main() { +#ifdef TEST_APPEND + AppendStructuredBuffer output = ResourceDescriptorHeap[0]; + output.Append(1); +#else + ConsumeStructuredBuffer input = ResourceDescriptorHeap[0]; + uint val = input.Consume(); +#endif +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl new file mode 100644 index 0000000000..eb0b6b25ca --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl @@ -0,0 +1,57 @@ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: StructuredBuffer / RWStructuredBuffer / ByteAddressBuffer +// share one StorageBuffer runtime array, yet each materializes its own +// typed OpBufferPointerEXT, ConstantBuffer uses a separate Uniform array, +// and reassignment re-indexes the shared array. +// +// StructuredBuffer -> shared OpTypeBufferEXT StorageBuffer array -> own typed OpBufferPointerEXT (%type_StructuredBuffer_*) +// RWStructuredBuffer -> shared OpTypeBufferEXT StorageBuffer array -> own typed OpBufferPointerEXT (%type_RWStructuredBuffer_*) +// ByteAddressBuffer -> shared OpTypeBufferEXT StorageBuffer array -> own typed OpBufferPointerEXT (%type_ByteAddressBuffer) +// ConstantBuffer -> separate OpTypeBufferEXT Uniform array -> own typed OpBufferPointerEXT (%type_ConstantBuffer_*) +// reassignment -> re-indexes shared StorageBuffer array (uint_4) -> same %type_StructuredBuffer_* pointer + +// CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] +// CHECK-DAG: %[[UBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform +// CHECK-DAG: %[[UBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[UBufDesc]] +// Anchor each pointer type to its struct-type prefix to prevent FileCheck from +// binding all three StorageBuffer captures to the scalar %_ptr_StorageBuffer_uint. +// CHECK-DAG: %[[SBInputPtr:[a-zA-Z0-9_]+]] = OpTypePointer StorageBuffer %type_StructuredBuffer_{{.*}} +// CHECK-DAG: %[[SBOutputPtr:[a-zA-Z0-9_]+]] = OpTypePointer StorageBuffer %type_RWStructuredBuffer_{{.*}} +// CHECK-DAG: %[[SBBytesPtr:[a-zA-Z0-9_]+]] = OpTypePointer StorageBuffer %type_ByteAddressBuffer{{$}} +// CHECK-DAG: %[[UConstPtr:[a-zA-Z0-9_]+]] = OpTypePointer Uniform %type_ConstantBuffer_{{.*}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant + +struct Constants { + uint value; +}; + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + StructuredBuffer input = ResourceDescriptorHeap[0]; + RWStructuredBuffer output = ResourceDescriptorHeap[1]; + ByteAddressBuffer inputBytes = ResourceDescriptorHeap[2]; + ConstantBuffer constants = ResourceDescriptorHeap[3]; + + // CHECK: %[[InputDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[SBBufArray]] %[[ResourceHeap]] %uint_0 + // CHECK: OpBufferPointerEXT %[[SBInputPtr]] %[[InputDesc]] + // CHECK: %[[OutputDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[SBBufArray]] %[[ResourceHeap]] %uint_1 + // CHECK: OpBufferPointerEXT %[[SBOutputPtr]] %[[OutputDesc]] + // CHECK: %[[InputBytesDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[SBBufArray]] %[[ResourceHeap]] %uint_2 + // CHECK: OpBufferPointerEXT %[[SBBytesPtr]] %[[InputBytesDesc]] + // CHECK: %[[ConstantsDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[UBufArray]] %[[ResourceHeap]] %uint_3 + // CHECK: OpBufferPointerEXT %[[UConstPtr]] %[[ConstantsDesc]] + output[tid.x] = input.Load(tid.x) + inputBytes.Load(tid.x * 4) + constants.value; + outputBytes.Store(tid.x * 4, output[tid.x]); + + // Reassignment: verify new descriptor (index 4) is used after reassign. + input = ResourceDescriptorHeap[4]; + // CHECK: %[[ReassignedDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[SBBufArray]] %[[ResourceHeap]] %uint_4 + // CHECK: OpBufferPointerEXT %[[SBInputPtr]] %[[ReassignedDesc]] + outputBytes.Store(tid.x * 4 + 4, input.Load(tid.x)); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-buffer-assignment.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-buffer-assignment.hlsl new file mode 100644 index 0000000000..3db71dcc13 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-buffer-assignment.hlsl @@ -0,0 +1,78 @@ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: ConstantBuffer reassignment from the descriptor heap re-indexes +// the Uniform array on every assignment and each member load uses the latest +// descriptor, in three forms that buffer.hlsl does not cover: +// +// dynamic runtime index -> %[[Idx]] (OpLoad %uint) + OpAccessChain %int_0 +// OpIAdd-computed index -> %[[IdxPlus2]] (OpIAdd) + OpAccessChain %int_1 +// reassignment inside a branch -> %[[IdxMinus2]] (OpUGreaterThan/OpBranchConditional/OpISub) + OpAccessChain %int_2 + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[BufferDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform +// CHECK-DAG: %[[BufferArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[BufferDesc]] +// CHECK-DAG: %[[CBPtr:[a-zA-Z0-9_]+]] = OpTypePointer Uniform %type_ConstantBuffer_Constants + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +struct Constants +{ + uint a; + uint b; + uint c; +}; + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) +{ + uint idx = tid.x; + uint cond = tid.y; + + ConstantBuffer constants = ResourceDescriptorHeap[idx]; + + // CHECK: %[[Idx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: %[[Cond:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: %[[InitDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[Idx]] + // CHECK: OpBufferPointerEXT %[[CBPtr]] %[[InitDesc]] + // CHECK: %[[ADesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[Idx]] + // CHECK: %[[APtr:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[ADesc]] + // CHECK: %[[AAccess:[a-zA-Z0-9_]+]] = OpAccessChain %{{[a-zA-Z0-9_]+}} %[[APtr]] %int_0 + // CHECK: %[[A:[a-zA-Z0-9_]+]] = OpLoad %uint %[[AAccess]] + uint value = constants.a; + + constants = ResourceDescriptorHeap[idx + 2]; + + // CHECK: %[[IdxPlus2:[a-zA-Z0-9_]+]] = OpIAdd %uint %[[Idx]] %uint_2 + // CHECK: %[[AssignDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[IdxPlus2]] + // CHECK: OpBufferPointerEXT %[[CBPtr]] %[[AssignDesc]] + // CHECK: %[[BDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[IdxPlus2]] + // CHECK: %[[BPtr:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[BDesc]] + // CHECK: %[[BUseDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[IdxPlus2]] + // CHECK: %[[BUsePtr:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[BUseDesc]] + // CHECK: %[[BAccess:[a-zA-Z0-9_]+]] = OpAccessChain %{{[a-zA-Z0-9_]+}} %[[BUsePtr]] %int_1 + // CHECK: %[[B:[a-zA-Z0-9_]+]] = OpLoad %uint %[[BAccess]] + // CHECK: %[[AB:[a-zA-Z0-9_]+]] = OpIAdd %uint %[[A]] %[[B]] + value += constants.b; + + if (cond > 4) { + constants = ResourceDescriptorHeap[idx - 2]; + + // CHECK: %[[CondCmp:[a-zA-Z0-9_]+]] = OpUGreaterThan %bool %[[Cond]] %uint_4 + // CHECK: OpBranchConditional %[[CondCmp]] + // CHECK: %[[IdxMinus2:[a-zA-Z0-9_]+]] = OpISub %uint %[[Idx]] %uint_2 + // CHECK: %[[BranchDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[IdxMinus2]] + // CHECK: OpBufferPointerEXT %[[CBPtr]] %[[BranchDesc]] + // CHECK: %[[CDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[IdxMinus2]] + // CHECK: %[[CPtr:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[CDesc]] + // CHECK: %[[CUseDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[BufferArray]] %[[ResourceHeap]] %[[IdxMinus2]] + // CHECK: %[[CUsePtr:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[CUseDesc]] + // CHECK: %[[CAccess:[a-zA-Z0-9_]+]] = OpAccessChain %{{[a-zA-Z0-9_]+}} %[[CUsePtr]] %int_2 + // CHECK: %[[C:[a-zA-Z0-9_]+]] = OpLoad %uint %[[CAccess]] + // CHECK: OpIAdd %uint %[[AB]] %[[C]] + value += constants.c; + } + + outputBytes.Store(0, value); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-texture-buffer.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-texture-buffer.hlsl new file mode 100644 index 0000000000..1a41f3ea44 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.constant-texture-buffer.hlsl @@ -0,0 +1,51 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: ConstantBuffer and TextureBuffer from the heap map to different element types, +// both carrying Block and supporting member access. +// +// ConstantBuffer -> OpTypeBufferEXT Uniform -> Block +// TextureBuffer -> OpTypeBufferEXT StorageBuffer -> Block + NonWritable members + +// CHECK-DAG: OpDecorate %type_ConstantBuffer_Data Block +// CHECK-DAG: OpDecorate %type_TextureBuffer_Data Block +// CHECK-DAG: OpMemberDecorate %type_TextureBuffer_Data 0 NonWritable +// CHECK-DAG: OpMemberDecorate %type_TextureBuffer_Data 1 NonWritable + +// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[CBDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform +// CHECK-DAG: %[[CBDescArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[CBDesc]] +// CHECK-DAG: %[[CBPtr:[a-zA-Z0-9_]+]] = OpTypePointer Uniform %type_ConstantBuffer_Data +// CHECK-DAG: %[[TBDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[TBDescArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TBDesc]] +// CHECK-DAG: %[[TBPtr:[a-zA-Z0-9_]+]] = OpTypePointer StorageBuffer %type_TextureBuffer_Data + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant + +struct Data { + uint a; + uint b[2]; +}; + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + ConstantBuffer cb = ResourceDescriptorHeap[0]; + TextureBuffer tb = ResourceDescriptorHeap[1]; + + uint index = tid.x & 1; + + // CHECK: %[[CBDescPtr:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[CBDescArray]] %[[ResourceHeap]] %uint_0 + // CHECK: OpBufferPointerEXT %[[CBPtr]] %[[CBDescPtr]] + // CHECK: %[[TBDescPtr:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[TBDescArray]] %[[ResourceHeap]] %uint_1 + // CHECK: OpBufferPointerEXT %[[TBPtr]] %[[TBDescPtr]] + // CHECK: %[[CBDataA:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[CBDescPtr]] + // CHECK: OpAccessChain %{{[a-zA-Z0-9_]+}} %[[CBDataA]] %int_0 + // CHECK: %[[CBDataB:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[CBPtr]] %[[CBDescPtr]] + // CHECK: OpAccessChain %{{[a-zA-Z0-9_]+}} %[[CBDataB]] %int_1 %{{[a-zA-Z0-9_]+}} + // CHECK: %[[TBDataA:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[TBPtr]] %[[TBDescPtr]] + // CHECK: OpAccessChain %{{[a-zA-Z0-9_]+}} %[[TBDataA]] %int_0 + // CHECK: %[[TBDataB:[a-zA-Z0-9_]+]] = OpBufferPointerEXT %[[TBPtr]] %[[TBDescPtr]] + // CHECK: OpAccessChain %{{[a-zA-Z0-9_]+}} %[[TBDataB]] %int_1 %int_1 + outputBytes.Store(0, cb.a + cb.b[index] + tb.a + tb.b[1]); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops.error.hlsl new file mode 100644 index 0000000000..c0b406428c --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops.error.hlsl @@ -0,0 +1,24 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_INCREMENT %s 2>&1 | FileCheck --check-prefix=INC %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck --check-prefix=DEC %s + +// Verifies: IncrementCounter/DecrementCounter on a +// heap-loaded RWStructuredBuffer is rejected with a hard error under +// SPV_EXT_descriptor_heap. + +// INC: counter operations on heap-loaded RWStructuredBuffer are not supported with SPV_EXT_descriptor_heap +// DEC: counter operations on heap-loaded RWStructuredBuffer are not supported with SPV_EXT_descriptor_heap + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main() { + RWStructuredBuffer buffer = ResourceDescriptorHeap[0]; + +#ifdef TEST_INCREMENT + uint value = buffer.IncrementCounter(); +#else + uint value = buffer.DecrementCounter(); +#endif + + outputBytes.Store(0, value); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.discarded.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.discarded.error.hlsl new file mode 100644 index 0000000000..3e26d746e4 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.discarded.error.hlsl @@ -0,0 +1,14 @@ +// RUN: not %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// Regression test for an unguarded cast in doCXXOperatorCallExpr: +// cast(parentMap->getParent(expr)) +// A descriptor-heap index whose result is discarded (used as a bare expression +// statement) has no parent cast to a concrete resource type, so the resource +// type cannot be determined. This must emit a diagnostic, not an ICE. + +// CHECK: error: {{.*}}ResourceDescriptorHeap/SamplerDescriptorHeap indexing must be used as a resource + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + ResourceDescriptorHeap[tid.x]; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.function-params.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.function-params.hlsl new file mode 100644 index 0000000000..3a72fa3482 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.function-params.hlsl @@ -0,0 +1,97 @@ +// RUN: %dxc -T cs_6_6 -E main -Od -fcgl -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: a heap-loaded resource passed by value to a user function lowers to +// pass-by-Function-pointer. +// +// heap handle -> OpStore into a Function-class var -> per-call OpLoad/OpStore/OpFunctionCall +// parameter slot -> OpFunctionParameter %[[*PtrType]] -> Function storage class + +// CHECK-DAG: OpName %[[ReadTex:[a-zA-Z0-9_]+]] "ReadTex" +// CHECK-DAG: OpName %[[ReadBuf:[a-zA-Z0-9_]+]] "ReadBuf" +// CHECK-DAG: OpName %[[WriteBuf:[a-zA-Z0-9_]+]] "WriteBuf" + +// CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[TexType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[BufType:[a-zA-Z0-9_]+]] = OpTypeImage %float Buffer 2 0 0 1 Rgba32f +// CHECK-DAG: %[[RWBufType:[a-zA-Z0-9_]+]] = OpTypeImage %float Buffer 2 0 0 2 Rgba32f +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[TexPtrType:[a-zA-Z0-9_]+]] = OpTypePointer Function %[[TexType]] +// CHECK-DAG: %[[BufPtrType:[a-zA-Z0-9_]+]] = OpTypePointer Function %[[BufType]] +// CHECK-DAG: %[[RWBufPtrType:[a-zA-Z0-9_]+]] = OpTypePointer Function %[[RWBufType]] +// CHECK-DAG: %[[SamplerPtrType:[a-zA-Z0-9_]+]] = OpTypePointer Function %[[SamplerType]] +// CHECK-DAG: %[[RA_TexType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexType]] +// CHECK-DAG: %[[RA_BufType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[BufType]] +// CHECK-DAG: %[[RA_RWBufType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWBufType]] +// CHECK-DAG: %[[RA_SamplerType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]] + +float4 ReadTex(Texture2D tex, SamplerState samp); +float4 ReadBuf(Buffer buf, uint index); +void WriteBuf(RWBuffer buf, uint index, float4 value); + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + Texture2D tex = ResourceDescriptorHeap[0]; + Buffer buf = ResourceDescriptorHeap[1]; + RWBuffer outBuf = ResourceDescriptorHeap[2]; + SamplerState samp = SamplerDescriptorHeap[0]; + + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant + // CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant + + // CHECK: %[[TexDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_TexType]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[TexHandle:[a-zA-Z0-9_]+]] = OpLoad %[[TexType]] %[[TexDesc]] + // CHECK: OpStore %[[TexVar:[a-zA-Z0-9_]+]] %[[TexHandle]] + // CHECK: %[[BufDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_BufType]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[BufHandle:[a-zA-Z0-9_]+]] = OpLoad %[[BufType]] %[[BufDesc]] + // CHECK: OpStore %[[BufVar:[a-zA-Z0-9_]+]] %[[BufHandle]] + // CHECK: %[[RWBufDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_RWBufType]] %[[ResourceHeap]] %uint_2 + // CHECK: %[[RWBufHandle:[a-zA-Z0-9_]+]] = OpLoad %[[RWBufType]] %[[RWBufDesc]] + // CHECK: OpStore %[[RWBufVar:[a-zA-Z0-9_]+]] %[[RWBufHandle]] + // CHECK: %[[SamplerDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_SamplerType]] %[[SamplerHeap]] %uint_0 + // CHECK: %[[SamplerHandle:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SamplerDesc]] + // CHECK: OpStore %[[SamplerVar:[a-zA-Z0-9_]+]] %[[SamplerHandle]] + + // CHECK: %[[TexArg:[a-zA-Z0-9_]+]] = OpLoad %[[TexType]] %[[TexVar]] + // CHECK: OpStore %[[TexParam:[a-zA-Z0-9_]+]] %[[TexArg]] + // CHECK: %[[SamplerArg:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SamplerVar]] + // CHECK: OpStore %[[SamplerParam:[a-zA-Z0-9_]+]] %[[SamplerArg]] + // CHECK: %[[TexValue:[a-zA-Z0-9_]+]] = OpFunctionCall %v4float %[[ReadTex]] %[[TexParam]] %[[SamplerParam]] + float4 value = ReadTex(tex, samp); + + // CHECK: %[[BufArg:[a-zA-Z0-9_]+]] = OpLoad %[[BufType]] %[[BufVar]] + // CHECK: OpStore %[[BufParam:[a-zA-Z0-9_]+]] %[[BufArg]] + // CHECK: %[[BufValue:[a-zA-Z0-9_]+]] = OpFunctionCall %v4float %[[ReadBuf]] %[[BufParam]] + value += ReadBuf(buf, tid.x); + + // CHECK: %[[RWBufArg:[a-zA-Z0-9_]+]] = OpLoad %[[RWBufType]] %[[RWBufVar]] + // CHECK: OpStore %[[RWBufParam:[a-zA-Z0-9_]+]] %[[RWBufArg]] + // CHECK: OpFunctionCall %void %[[WriteBuf]] %[[RWBufParam]] + WriteBuf(outBuf, tid.x, value); + + outputBytes.Store(tid.x * 4, asuint(value.x)); +} + +// Callees emit after the entry point; each receives its resource by Function pointer. +float4 ReadTex(Texture2D tex, SamplerState samp) { + // CHECK: %[[ReadTex]] = OpFunction %v4float + // CHECK: OpFunctionParameter %[[TexPtrType]] + // CHECK: OpFunctionParameter %[[SamplerPtrType]] + // CHECK: OpSampledImage + return tex.SampleLevel(samp, float2(0.0, 0.0), 0.0); +} + +float4 ReadBuf(Buffer buf, uint index) { + // CHECK: %[[ReadBuf]] = OpFunction %v4float + // CHECK: OpFunctionParameter %[[BufPtrType]] + // CHECK: OpImageFetch + return buf.Load(index); +} + +void WriteBuf(RWBuffer buf, uint index, float4 value) { + // CHECK: %[[WriteBuf]] = OpFunction %void + // CHECK: OpFunctionParameter %[[RWBufPtrType]] + // CHECK: OpImageWrite + buf[index] = value; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl new file mode 100644 index 0000000000..9b1a720201 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl @@ -0,0 +1,42 @@ +// RUN: %dxc -T ps_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: GatherRed/Green/Blue/Alpha each emit a fresh OpSampledImage that +// reuses the once-loaded heap-sourced texture and sampler handles, selecting +// channel %int_0/1/2/3 via OpImageGather %v4float. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[SampledImgType:[a-zA-Z0-9_]+]] = OpTypeSampledImage %[[Tex2DType]] +// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} + +// CHECK-DAG: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK-DAG: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +float4 main(float2 uv : TEXCOORD0) : SV_Target { + Texture2D tex = ResourceDescriptorHeap[0]; + SamplerState samp = SamplerDescriptorHeap[0]; + + // Tex and sampler are each loaded once from the heap; each Gather* creates a + // fresh OpSampledImage but reuses the same loaded image and sampler handles. + // CHECK: %[[TexChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[TexH:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] %[[TexChain]] + // CHECK: %[[SampChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_0 + // CHECK: %[[SampH:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampChain]] + + // CHECK: %[[SI0:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImgType]] %[[TexH]] %[[SampH]] + // CHECK: OpImageGather %v4float %[[SI0]] {{.*}} %int_0 + float4 r = tex.GatherRed(samp, uv); + // CHECK: %[[SI1:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImgType]] %[[TexH]] %[[SampH]] + // CHECK: OpImageGather %v4float %[[SI1]] {{.*}} %int_1 + float4 g = tex.GatherGreen(samp, uv); + // CHECK: %[[SI2:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImgType]] %[[TexH]] %[[SampH]] + // CHECK: OpImageGather %v4float %[[SI2]] {{.*}} %int_2 + float4 b = tex.GatherBlue(samp, uv); + // CHECK: %[[SI3:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImgType]] %[[TexH]] %[[SampH]] + // CHECK: OpImageGather %v4float %[[SI3]] {{.*}} %int_3 + float4 a = tex.GatherAlpha(samp, uv); + + return r + g + b + a; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.groupshared.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.groupshared.hlsl new file mode 100644 index 0000000000..f68e7b6b59 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.groupshared.hlsl @@ -0,0 +1,35 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: heap StructuredBuffer/RWStructuredBuffer coexist with groupshared +// (Workgroup storage class) and GroupMemoryBarrierWithGroupSync. +// 1) groupshared backed by OpTypePointer Workgroup, distinct from heap descriptor pointers. +// 2) GroupMemoryBarrierWithGroupSync lowers to OpControlBarrier alongside heap accesses. +// 3) heap SB/RWSB accessed via OpUntypedAccessChainKHR + OpBufferPointerEXT in same entry point. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] + +// groupshared lives in Workgroup storage class. +// CHECK-DAG: %[[WorkgroupPtr:[a-zA-Z0-9_]+]] = OpTypePointer Workgroup +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +groupshared float4 shared_data[64]; + +[numthreads(64, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID, uint gi : SV_GroupIndex) { + StructuredBuffer input = ResourceDescriptorHeap[0]; + RWStructuredBuffer output = ResourceDescriptorHeap[1]; + + // CHECK-DAG: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SBBufArray]] %[[ResourceHeap]] %uint_0 + // CHECK-DAG: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SBBufArray]] %[[ResourceHeap]] %uint_1 + shared_data[gi] = input[tid.x]; + + // CHECK: OpControlBarrier + GroupMemoryBarrierWithGroupSync(); + + uint neighbor = (gi + 1) % 64; + + // CHECK: OpBufferPointerEXT + output[tid.x] = shared_data[gi] + shared_data[neighbor]; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.load-offset.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.load-offset.hlsl new file mode 100644 index 0000000000..a2390ee75e --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.load-offset.hlsl @@ -0,0 +1,27 @@ +// RUN: %dxc -T ps_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: Texture.Load with a constant offset emits the ConstOffset +// image operand on OpImageFetch, contrasted against the no-offset fetch baseline. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} + +float4 main(float4 pos : SV_Position) : SV_Target { + Texture2D tex = ResourceDescriptorHeap[0]; + + // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap:[a-zA-Z0-9_]+]] %uint_0 + // CHECK: %[[Handle:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] + + int3 coord = int3(pos.xy, 0); + + // Load without offset. + // CHECK: OpImageFetch %v4float %[[Handle]] + float4 a = tex.Load(coord); + + // Load with constant offset — the ConstOffset image operand must appear. + // CHECK: OpImageFetch %v4float %[[Handle]] {{.*}}ConstOffset + float4 b = tex.Load(coord, int2(1, -1)); + + return a + b; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-bound.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-bound.hlsl new file mode 100644 index 0000000000..25089d9e43 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-bound.hlsl @@ -0,0 +1,54 @@ +// RUN: %dxc -T ps_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: explicitly-bound resources coexist with descriptor-heap resources, and a bound image combines with a heap sampler. +// register(t0) Texture2D -> OpVariable %[[PtrTex]] UniformConstant + DescriptorSet/Binding -> explicitly-bound resource +// ResourceHeap/SamplerHeap -> OpUntypedVariableKHR UniformConstant (BuiltIn ResourceHeapEXT/SamplerHeapEXT) -> heap resources +// bound image + heap sampler -> OpSampledImage %[[BoundVal]] %[[SampH]] -> bound-image/heap-sampler combination + +// CHECK: OpCapability DescriptorHeapEXT +// CHECK-NOT: OpCapability UntypedPointersKHR +// CHECK: OpExtension "SPV_EXT_descriptor_heap" +// CHECK: OpExtension "SPV_KHR_untyped_pointers" + +// CHECK-DAG: OpDecorate %[[ResourceHeap:[a-zA-Z0-9_]+]] BuiltIn ResourceHeapEXT +// CHECK-DAG: OpDecorate %[[SamplerHeap:[a-zA-Z0-9_]+]] BuiltIn SamplerHeapEXT + +// CHECK-DAG: OpDecorate %[[BoundTex:[a-zA-Z0-9_]+]] DescriptorSet +// CHECK-DAG: OpDecorate %[[BoundTex]] Binding + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} +// CHECK-DAG: %[[PtrTex:[a-zA-Z0-9_]+]] = OpTypePointer UniformConstant %[[Tex2DType]] + +// CHECK: %[[BoundTex]] = OpVariable %[[PtrTex]] UniformConstant +// CHECK-DAG: %[[ResourceHeap]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK-DAG: %[[SamplerHeap]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +[[vk::binding(0, 0)]] +Texture2D boundTex : register(t0); + +float4 main(float2 uv : TEXCOORD0) : SV_Target { + Texture2D heapTex = ResourceDescriptorHeap[1]; + SamplerState samp = SamplerDescriptorHeap[0]; + + // CHECK: %[[HeapDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[HeapVal:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] %[[HeapDesc]] + + // One heap sampler drives both samples, proving heap-sampler reuse across bound and heap images. + // CHECK: %[[SampChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_0 + // CHECK: %[[SampH:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampChain]] + + // CHECK: %[[BoundVal:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] %[[BoundTex]] + + // OWNED: bound image (OpVariable) combined with heap sampler. + // CHECK: OpSampledImage %{{.*}} %[[BoundVal]] %[[SampH]] + float4 a = boundTex.Sample(samp, uv); + + // CHECK: OpSampledImage %{{.*}} %[[HeapVal]] %[[SampH]] + float4 b = heapTex.Sample(samp, uv + 0.5); + + return a + b; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl new file mode 100644 index 0000000000..591fac84ff --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl @@ -0,0 +1,30 @@ +// RUN: %dxc -T ps_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: NonUniformResourceIndex on descriptor-heap accesses does NOT +// propagate NonUniform to the access-chain result or the loaded value. +// SPV_EXT_descriptor_heap deprecates the NonUniform decoration; drivers handle +// divergent indices natively and the decoration must be omitted. + +// CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[RA_Tex2DType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]] +// CHECK-DAG: %[[RA_SamplerType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]] + +// CHECK-NOT: OpDecorate %{{.*}} NonUniform + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant +// CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant + +float4 main(uint idx : A) : SV_Target { + Texture2D tex = ResourceDescriptorHeap[NonUniformResourceIndex(idx)]; + SamplerState samp = SamplerDescriptorHeap[NonUniformResourceIndex(idx + 1)]; + + // CHECK: %[[TexChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_Tex2DType]] %[[ResourceHeap]] %{{.*}} + // CHECK: %[[TexLoaded:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] %[[TexChain]] + // CHECK: %[[SampChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_SamplerType]] %[[SamplerHeap]] %{{.*}} + // CHECK: %[[SampLoaded:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampChain]] + // CHECK: %[[Combined:[a-zA-Z0-9_]+]] = OpSampledImage %{{.*}} %[[TexLoaded]] %[[SampLoaded]] + // CHECK: OpImageSampleExplicitLod %v4float %[[Combined]] + return tex.SampleLevel(samp, float2(0.0, 0.0), 0.0); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwbyteaddressbuffer.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwbyteaddressbuffer.hlsl new file mode 100644 index 0000000000..3e84ff294d --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwbyteaddressbuffer.hlsl @@ -0,0 +1,21 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: an RWByteAddressBuffer sourced from the descriptor heap +// produces the typed StorageBuffer pointer %type_RWByteAddressBuffer +// and a read-WRITE OpBufferPointerEXT through it. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] +// CHECK-DAG: %[[RWBABPtr:[a-zA-Z0-9_]+]] = OpTypePointer StorageBuffer %type_RWByteAddressBuffer +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +[numthreads(64, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWByteAddressBuffer buf = ResourceDescriptorHeap[0]; + + // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SBBufArray]] %[[ResourceHeap]] %uint_0 + // CHECK: OpBufferPointerEXT %[[RWBABPtr]] %[[Desc]] + uint val = buf.Load(tid.x * 4); + buf.Store(tid.x * 4 + 256, val + 1); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-atomics.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-atomics.hlsl new file mode 100644 index 0000000000..6937f828ef --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-atomics.hlsl @@ -0,0 +1,53 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: InterlockedAdd on a heap-sourced RWTexture2D lowers +// to OpUntypedImageTexelPointerEXT (not OpImageTexelPointer) +// feeding OpAtomicIAdd, and reassignment targets the new descriptor. +// +// RWTexture2D heap access -> OpUntypedAccessChainKHR -> descriptor pointer in resource heap +// atomic texel pointer -> OpUntypedImageTexelPointerEXT -> EXT untyped image texel pointer +// InterlockedAdd -> OpAtomicIAdd -> image atomic on untyped texel pointer +// reassignment -> OpUntypedAccessChainKHR %uint_3 -> atomic uses new descriptor index + +// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[RWTexType:[a-zA-Z0-9_]+]] = OpTypeImage %uint 2D 2 0 0 2 R32ui +// CHECK-DAG: %[[RWTexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexType]] +// CHECK-DAG: %[[UntypedImage:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR Image + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture2D tex = ResourceDescriptorHeap[0]; + + uint original; + // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_0 + // CHECK-NOT: OpImageTexelPointer + // CHECK: %[[TexelPtr:[a-zA-Z0-9_]+]] = OpUntypedImageTexelPointerEXT %[[UntypedImage]] %[[RWTexType]] %[[Desc]] + // CHECK: OpAtomicIAdd %uint %[[TexelPtr]] + InterlockedAdd(tex[tid.xy], 1, original); + + uint directOriginal; + // CHECK: %[[DirectDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_1 + // CHECK-NOT: OpImageTexelPointer + // CHECK: %[[DirectTexelPtr:[a-zA-Z0-9_]+]] = OpUntypedImageTexelPointerEXT %[[UntypedImage]] %[[RWTexType]] %[[DirectDesc]] + // CHECK: OpAtomicIAdd %uint %[[DirectTexelPtr]] + InterlockedAdd(((RWTexture2D)ResourceDescriptorHeap[1])[tid.xy], 2, + directOriginal); + + // Reassignment: atomic must use the NEW descriptor (index 3, not 2). + RWTexture2D reassigned = ResourceDescriptorHeap[2]; + reassigned = ResourceDescriptorHeap[3]; + uint reassignedOriginal; + // CHECK: %[[ReassignDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_3 + // CHECK-NOT: OpImageTexelPointer + // CHECK: %[[ReassignTexelPtr:[a-zA-Z0-9_]+]] = OpUntypedImageTexelPointerEXT %[[UntypedImage]] %[[RWTexType]] %[[ReassignDesc]] + // CHECK: OpAtomicIAdd %uint %[[ReassignTexelPtr]] + InterlockedAdd(reassigned[tid.xy], 3, reassignedOriginal); + + outputBytes.Store(0, original); + outputBytes.Store(4, directOriginal); + outputBytes.Store(8, reassignedOriginal); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl new file mode 100644 index 0000000000..87d2591318 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl @@ -0,0 +1,53 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: storage RW texture dimensionality lowers to distinct +// OpTypeImage dim/flags, each driving OpImageRead/OpImageWrite. +// +// RWTexture1D -> OpTypeImage %float 1D 2 0 0 2 Rgba32f -> storage +// RWTexture1DArray -> OpTypeImage %float 1D 2 1 0 2 Rgba32f -> storage +// RWTexture2DArray -> OpTypeImage %float 2D 2 1 0 2 Rgba32f -> storage +// RWTexture3D -> OpTypeImage %float 3D 2 0 0 2 Rgba32f -> storage + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[RW1DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 1D 2 0 0 2 Rgba32f +// CHECK-DAG: %[[RW1DArrType:[a-zA-Z0-9_]+]] = OpTypeImage %float 1D 2 1 0 2 Rgba32f +// CHECK-DAG: %[[RW2DArrType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 1 0 2 Rgba32f +// CHECK-DAG: %[[RW3DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 3D 2 0 0 2 Rgba32f + +// CHECK-DAG: %[[RA_RW1D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW1DType]]{{$}} +// CHECK-DAG: %[[RA_RW1DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW1DArrType]]{{$}} +// CHECK-DAG: %[[RA_RW2DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW2DArrType]]{{$}} +// CHECK-DAG: %[[RA_RW3D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW3DType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture1D rwTex1d = ResourceDescriptorHeap[0]; + // CHECK: %[[RW1D_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RW1D]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[RW1D:[a-zA-Z0-9_]+]] = OpLoad %[[RW1DType]] %[[RW1D_Desc]] + + RWTexture1DArray rwTex1dArr = ResourceDescriptorHeap[1]; + // CHECK: %[[RW1DA_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RW1DArr]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[RW1DA:[a-zA-Z0-9_]+]] = OpLoad %[[RW1DArrType]] %[[RW1DA_Desc]] + + RWTexture2DArray rwTex2dArr = ResourceDescriptorHeap[2]; + // CHECK: %[[RW2DA_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RW2DArr]] %[[ResourceHeap]] %uint_2 + // CHECK: %[[RW2DA:[a-zA-Z0-9_]+]] = OpLoad %[[RW2DArrType]] %[[RW2DA_Desc]] + + RWTexture3D rwTex3d = ResourceDescriptorHeap[3]; + // CHECK: %[[RW3D_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RW3D]] %[[ResourceHeap]] %uint_3 + // CHECK: %[[RW3D:[a-zA-Z0-9_]+]] = OpLoad %[[RW3DType]] %[[RW3D_Desc]] + + // CHECK: OpImageRead %v4float %[[RW1D]] + float4 v = rwTex1d[tid.x]; + + // CHECK: OpImageWrite %[[RW1DA]] + rwTex1dArr[uint2(tid.x, 0)] = v; + + // CHECK: OpImageWrite %[[RW2DA]] + rwTex2dArr[uint3(tid.xy, 0)] = v; + + // CHECK: OpImageWrite %[[RW3D]] + rwTex3d[tid.xyz] = v; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sample-grad-bias.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sample-grad-bias.hlsl new file mode 100644 index 0000000000..756dd53e2d --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sample-grad-bias.hlsl @@ -0,0 +1,40 @@ +// RUN: %dxc -T ps_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: SampleGrad lowers to OpImageSampleExplicitLod with the Grad image +// operand and SampleBias lowers to OpImageSampleImplicitLod with the Bias +// image operand, where the texture and sampler handles are loaded once from +// the descriptor heaps and reused across both samples (each sample a fresh +// OpSampledImage). + +// Capture anchors only (heap-source decorations owned by mixed-bound). +// CHECK-DAG: OpDecorate %[[ResourceHeap:[a-zA-Z0-9_]+]] BuiltIn ResourceHeapEXT +// CHECK-DAG: OpDecorate %[[SamplerHeap:[a-zA-Z0-9_]+]] BuiltIn SamplerHeapEXT + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[SampledImgType:[a-zA-Z0-9_]+]] = OpTypeSampledImage %[[Tex2DType]] +// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} + +float4 main(float2 uv : TEXCOORD0) : SV_Target { + Texture2D tex = ResourceDescriptorHeap[0]; + SamplerState samp = SamplerDescriptorHeap[0]; + + // Tex and sampler each loaded once; each sample op creates its own OpSampledImage + // but both reuse the same loaded handles. + // CHECK: %[[TexChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[TexH:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DType]] %[[TexChain]] + // CHECK: %[[SampChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_0 + // CHECK: %[[SampH:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampChain]] + + // CHECK: %[[SI0:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImgType]] %[[TexH]] %[[SampH]] + // CHECK: OpImageSampleExplicitLod %v4float %[[SI0]] {{.*}} Grad + float4 a = tex.SampleGrad(samp, uv, ddx(uv), ddy(uv)); + + // CHECK: %[[SI1:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImgType]] %[[TexH]] %[[SampH]] + // CHECK: OpImageSampleImplicitLod %v4float %[[SI1]] {{.*}} Bias + float4 b = tex.SampleBias(samp, uv, -1.0); + + return a + b; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sampler-comparison.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sampler-comparison.hlsl new file mode 100644 index 0000000000..f5a118715e --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.sampler-comparison.hlsl @@ -0,0 +1,30 @@ +// RUN: %dxc -T ps_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: a heap-sourced SamplerComparisonState combined with a heap-sourced +// Texture2D drives OpImageSampleDrefExplicitLod (SampleCmpLevelZero), +// with the comparison sampler loaded from the sampler heap and joined +// via OpSampledImage. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[TexType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[RA_Tex:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +float4 main(float2 uv : TEXCOORD0) : SV_Target { + Texture2D depthTex = ResourceDescriptorHeap[0]; + // CHECK: %[[TexDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[TexHandle:[a-zA-Z0-9_]+]] = OpLoad %[[TexType]] %[[TexDesc]] + + SamplerComparisonState shadowSamp = SamplerDescriptorHeap[0]; + // CHECK: %[[SampDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_0 + // CHECK: %[[SampHandle:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampDesc]] + + // CHECK: %[[Combined:[a-zA-Z0-9_]+]] = OpSampledImage %{{[a-zA-Z0-9_]+}} %[[TexHandle]] %[[SampHandle]] + // CHECK: %[[Shadow:[a-zA-Z0-9_]+]] = OpImageSampleDrefExplicitLod %float %[[Combined]] + float shadow = depthTex.SampleCmpLevelZero(shadowSamp, uv, 0.5); + return float4(shadow, shadow, shadow, 1.0); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl new file mode 100644 index 0000000000..e12cf41076 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl @@ -0,0 +1,64 @@ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: UE bindless idiom — static const resources at global scope +// initialized from dynamic runtime uint indices ($Globals), across +// Texture2D / StructuredBuffer / RWTexture2D, plus a literal-index +// SamplerState. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[RWTex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 2 Rgba32f + +// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} +// CHECK-DAG: %[[RA_SBBuf:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]]{{$}} +// CHECK-DAG: %[[RA_RWTex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTex2DType]]{{$}} + +// CHECK-DAG: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK-DAG: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +// Runtime uint indices (placed in $Globals cbuffer by DXC). +uint BindlessSRV_ColorTex; +uint BindlessSRV_DataBuf; +uint BindlessUAV_OutTex; + +// UE bindless pattern: static const from runtime heap index. +static const Texture2D ColorTex = ResourceDescriptorHeap[BindlessSRV_ColorTex]; +static const StructuredBuffer DataBuf = ResourceDescriptorHeap[BindlessSRV_DataBuf]; +static const RWTexture2D OutTex = ResourceDescriptorHeap[BindlessUAV_OutTex]; + +// Literal-index sampler (common UE pattern for global bilinear/point samplers). +static const SamplerState BilinearSamp = SamplerDescriptorHeap[2]; + +[numthreads(8, 8, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + // Heap indices are dynamic (loaded from $Globals), not literal constants. + // CHECK: %[[TexIdx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: %[[TexDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap]] %[[TexIdx]] + // CHECK: OpLoad %[[Tex2DType]] %[[TexDesc]] + + // CHECK: %[[BufIdx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_SBBuf]] %[[ResourceHeap]] %[[BufIdx]] + // CHECK: OpBufferPointerEXT + + // CHECK: %[[OutIdx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: %[[OutDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RWTex2D]] %[[ResourceHeap]] %[[OutIdx]] + // CHECK: OpLoad %[[RWTex2DType]] %[[OutDesc]] + + // Literal sampler index. + // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_2 + // CHECK: OpLoad %[[SamplerType]] + + float2 uv = float2(tid.xy) / 512.0; + + // CHECK: OpSampledImage + // CHECK: OpImageSampleExplicitLod + float4 color = ColorTex.SampleLevel(BilinearSamp, uv, 0); + + float4 data = DataBuf[tid.x]; + + // CHECK: OpImageWrite %{{[a-zA-Z0-9_]+}} + OutTex[tid.xy] = color + data; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.structured-buffer-atomic.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.structured-buffer-atomic.hlsl new file mode 100644 index 0000000000..bbf811e6ca --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.structured-buffer-atomic.hlsl @@ -0,0 +1,28 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: BUFFER atomics on a heap-sourced RWStructuredBuffer +// lower through a heap access-chain + OpBufferPointerEXT into +// OpAtomicIAdd/OpAtomicCompareExchange on %uint. +// +// RWStructuredBuffer -> heap access-chain + OpBufferPointerEXT -> OpAtomicIAdd %uint +// RWStructuredBuffer -> heap access-chain + OpBufferPointerEXT -> OpAtomicCompareExchange %uint + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +[numthreads(64, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWStructuredBuffer counter = ResourceDescriptorHeap[0]; + + // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SBBufArray]] %[[ResourceHeap]] %uint_0 + // CHECK: OpBufferPointerEXT + uint original; + // CHECK: OpAtomicIAdd %uint + InterlockedAdd(counter[0], 1, original); + + // CHECK: OpAtomicCompareExchange %uint + uint cmp; + InterlockedCompareExchange(counter[1], original, original + 1, cmp); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl new file mode 100644 index 0000000000..120830aa4c --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl @@ -0,0 +1,54 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: sampled (non-MS, non-cube) texture dimensionalities lower +// to the correct OpTypeImage dim flags and feed OpImageFetch. +// +// Texture1D -> OpTypeImage %float 1D 2 0 0 1 Unknown -> sampled +// Texture1DArray -> OpTypeImage %float 1D 2 1 0 1 Unknown -> sampled +// Texture2DArray -> OpTypeImage %float 2D 2 1 0 1 Unknown -> sampled +// Texture3D -> OpTypeImage %float 3D 2 0 0 1 Unknown -> sampled + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex1DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 1D 2 0 0 1 Unknown +// CHECK-DAG: %[[Tex1DArrType:[a-zA-Z0-9_]+]] = OpTypeImage %float 1D 2 1 0 1 Unknown +// CHECK-DAG: %[[Tex2DArrType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 1 0 1 Unknown +// CHECK-DAG: %[[Tex3DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 3D 2 0 0 1 Unknown + +// CHECK-DAG: %[[RA_Tex1D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex1DType]]{{$}} +// CHECK-DAG: %[[RA_Tex1DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex1DArrType]]{{$}} +// CHECK-DAG: %[[RA_Tex2DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DArrType]]{{$}} +// CHECK-DAG: %[[RA_Tex3D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex3DType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +RWByteAddressBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + Texture1D tex1d = ResourceDescriptorHeap[0]; + // CHECK: %[[T1D_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex1D]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[T1D:[a-zA-Z0-9_]+]] = OpLoad %[[Tex1DType]] %[[T1D_Desc]] + + Texture1DArray tex1dArr = ResourceDescriptorHeap[1]; + // CHECK: %[[T1DA_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex1DArr]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[T1DA:[a-zA-Z0-9_]+]] = OpLoad %[[Tex1DArrType]] %[[T1DA_Desc]] + + Texture2DArray tex2dArr = ResourceDescriptorHeap[2]; + // CHECK: %[[T2DA_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2DArr]] %[[ResourceHeap]] %uint_2 + // CHECK: %[[T2DA:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2DArrType]] %[[T2DA_Desc]] + + Texture3D tex3d = ResourceDescriptorHeap[3]; + // CHECK: %[[T3D_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex3D]] %[[ResourceHeap]] %uint_3 + // CHECK: %[[T3D:[a-zA-Z0-9_]+]] = OpLoad %[[Tex3DType]] %[[T3D_Desc]] + + // CHECK: OpImageFetch %v4float %[[T1D]] + float4 v = tex1d.Load(int2(tid.x, 0)); + // CHECK: OpImageFetch %v4float %[[T1DA]] + v += tex1dArr.Load(int3(tid.x, 0, 0)); + // CHECK: OpImageFetch %v4float %[[T2DA]] + v += tex2dArr.Load(int4(tid.xy, 0, 0)); + // CHECK: OpImageFetch %v4float %[[T3D]] + v += tex3d.Load(int4(tid.xyz, 0)); + + output.Store(0, asuint(v.x)); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl new file mode 100644 index 0000000000..edb2d78821 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl @@ -0,0 +1,33 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: multisampled image types (Texture2DMS `%float 2D 2 0 1 1` and Texture2DMSArray `2D 2 1 1 1`) +// sourced from the descriptor heap are loaded and drive OpImageFetch %v4float. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[MSType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 1 1 Unknown +// CHECK-DAG: %[[MSArrType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 1 1 1 Unknown + +// CHECK-DAG: %[[RA_MS:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[MSType]]{{$}} +// CHECK-DAG: %[[RA_MSArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[MSArrType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +RWByteAddressBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + Texture2DMS texMS = ResourceDescriptorHeap[0]; + // CHECK: %[[MS_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_MS]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[MS:[a-zA-Z0-9_]+]] = OpLoad %[[MSType]] %[[MS_Desc]] + + Texture2DMSArray texMSArr = ResourceDescriptorHeap[1]; + // CHECK: %[[MSArr_Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_MSArr]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[MSArr:[a-zA-Z0-9_]+]] = OpLoad %[[MSArrType]] %[[MSArr_Desc]] + + // CHECK: OpImageFetch %v4float %[[MS]] + float4 v = texMS.Load(int2(tid.xy), 0); + // CHECK: OpImageFetch %v4float %[[MSArr]] + v += texMSArr.Load(int3(tid.xy, 0), 0); + + output.Store(0, asuint(v.x)); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-sampler-assignment.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-sampler-assignment.hlsl new file mode 100644 index 0000000000..6b4297741e --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-sampler-assignment.hlsl @@ -0,0 +1,77 @@ +// RUN: %dxc -T ps_6_6 -E PSMain -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: reassigning both a Texture2D and a SamplerState across a +// dynamic index, a +2 index, and inside a conditional branch reloads +// the latest texture+sampler from BOTH the resource and sampler +// heaps per reassignment, producing a fresh +// OpSampledImage/OpImageSampleImplicitLod each time. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2D:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[Sampler:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[TexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2D]] +// CHECK-DAG: %[[SamplerArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Sampler]] +// CHECK-DAG: %[[SampledImage:[a-zA-Z0-9_]+]] = OpTypeSampledImage %[[Tex2D]] + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +struct PSInput +{ + float4 position : SV_Position; + float4 color : COLOR0; +}; + +float4 PSMain(PSInput input) : SV_Target0 +{ + uint texIdx = uint(input.color.x); + uint sampIdx = uint(input.color.y); + uint cond = uint(input.color.z); + + Texture2D myTexture = ResourceDescriptorHeap[texIdx]; + SamplerState samp = SamplerDescriptorHeap[sampIdx]; + + // CHECK: %[[TexIdx:[a-zA-Z0-9_]+]] = OpConvertFToU %uint + // CHECK: %[[SampIdx:[a-zA-Z0-9_]+]] = OpConvertFToU %uint + // CHECK: %[[Cond:[a-zA-Z0-9_]+]] = OpConvertFToU %uint + // CHECK: %[[TexDesc0:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[TexArray]] %[[ResourceHeap]] %[[TexIdx]] + // CHECK: %[[Tex0:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2D]] %[[TexDesc0]] + // CHECK: %[[SampDesc0:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SamplerArray]] %[[SamplerHeap]] %[[SampIdx]] + // CHECK: %[[Samp0:[a-zA-Z0-9_]+]] = OpLoad %[[Sampler]] %[[SampDesc0]] + // CHECK: %[[Sampled0:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImage]] %[[Tex0]] %[[Samp0]] + // CHECK: %[[Color0:[a-zA-Z0-9_]+]] = OpImageSampleImplicitLod %v4float %[[Sampled0]] + float4 color = myTexture.Sample(samp, float2(1,1)); + + myTexture = ResourceDescriptorHeap[texIdx + 2]; + samp = SamplerDescriptorHeap[sampIdx]; + + // CHECK: %[[TexIdxPlus2:[a-zA-Z0-9_]+]] = OpIAdd %uint %[[TexIdx]] %uint_2 + // CHECK: %[[TexDesc1:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[TexArray]] %[[ResourceHeap]] %[[TexIdxPlus2]] + // CHECK: %[[Tex1:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2D]] %[[TexDesc1]] + // CHECK: %[[SampDesc1:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SamplerArray]] %[[SamplerHeap]] %[[SampIdx]] + // CHECK: %[[Samp1:[a-zA-Z0-9_]+]] = OpLoad %[[Sampler]] %[[SampDesc1]] + // CHECK: %[[Sampled1:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImage]] %[[Tex1]] %[[Samp1]] + // CHECK: %[[Color1:[a-zA-Z0-9_]+]] = OpImageSampleImplicitLod %v4float %[[Sampled1]] + // CHECK: %[[ColorSum:[a-zA-Z0-9_]+]] = OpFAdd %v4float %[[Color0]] %[[Color1]] + color += myTexture.Sample(samp, float2(2,2)); + + if (cond > 4) { + myTexture = ResourceDescriptorHeap[texIdx - 2]; + samp = SamplerDescriptorHeap[sampIdx + 2]; + + // CHECK: %[[CondCmp:[a-zA-Z0-9_]+]] = OpUGreaterThan %bool %[[Cond]] %uint_4 + // CHECK: OpBranchConditional %[[CondCmp]] + // CHECK: %[[TexIdxMinus2:[a-zA-Z0-9_]+]] = OpISub %uint %[[TexIdx]] %uint_2 + // CHECK: %[[TexDesc2:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[TexArray]] %[[ResourceHeap]] %[[TexIdxMinus2]] + // CHECK: %[[Tex2:[a-zA-Z0-9_]+]] = OpLoad %[[Tex2D]] %[[TexDesc2]] + // CHECK: %[[SampIdxPlus2:[a-zA-Z0-9_]+]] = OpIAdd %uint %[[SampIdx]] %uint_2 + // CHECK: %[[SampDesc2:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[SamplerArray]] %[[SamplerHeap]] %[[SampIdxPlus2]] + // CHECK: %[[Samp2:[a-zA-Z0-9_]+]] = OpLoad %[[Sampler]] %[[SampDesc2]] + // CHECK: %[[Sampled2:[a-zA-Z0-9_]+]] = OpSampledImage %[[SampledImage]] %[[Tex2]] %[[Samp2]] + // CHECK: %[[Color2:[a-zA-Z0-9_]+]] = OpImageSampleImplicitLod %v4float %[[Sampled2]] + // CHECK: OpFAdd %v4float %[[ColorSum]] %[[Color2]] + color += myTexture.Sample(samp, float2(2,2)); + } + + return color; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl new file mode 100644 index 0000000000..6d769383f6 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl @@ -0,0 +1,34 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: Buffer and RWBuffer from the heap lower to +// Buffer-dimension OpTypeImage (sampled vs storage), +// load a typed image handle, and drive OpImageFetch / OpImageWrite. +// +// Buffer -> OpTypeImage %float Buffer ... Sampled(1), handle OpLoad, OpImageFetch (Load) +// RWBuffer -> OpTypeImage %float Buffer ... Storage(2), handle OpLoad, OpImageWrite (store) + +// CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[BufferType:[a-zA-Z0-9_]+]] = OpTypeImage %float Buffer 2 0 0 1 Rgba32f +// CHECK-DAG: %[[RWBufferType:[a-zA-Z0-9_]+]] = OpTypeImage %float Buffer 2 0 0 2 Rgba32f + +// CHECK-DAG: %[[RA_BufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[BufferType]]{{$}} +// CHECK-DAG: %[[RA_RWBufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWBufferType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + Buffer myBuf = ResourceDescriptorHeap[0]; + // CHECK: %[[BufIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_BufferType]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[BufHandle:[a-zA-Z0-9_]+]] = OpLoad %[[BufferType]] %[[BufIndex]] + + RWBuffer myRWBuf = ResourceDescriptorHeap[1]; + // CHECK: %[[RWBufIndex:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtrType]] %[[RA_RWBufferType]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[RWBufHandle:[a-zA-Z0-9_]+]] = OpLoad %[[RWBufferType]] %[[RWBufIndex]] + + // CHECK: %[[BufResult:[a-zA-Z0-9_]+]] = OpImageFetch %v4float %[[BufHandle]] + float4 bufVal = myBuf.Load(tid.x); + + // CHECK: OpImageWrite %[[RWBufHandle]] + myRWBuf[tid.x] = bufVal; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl new file mode 100644 index 0000000000..237fed4aa9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl @@ -0,0 +1,46 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: TextureCube and TextureCubeArray cube image types, +// each loaded from the resource heap and combined with a heap +// sampler to drive OpImageSampleExplicitLod. + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[CubeType:[a-zA-Z0-9_]+]] = OpTypeImage %float Cube 2 0 0 1 Unknown +// CHECK-DAG: %[[CubeArrType:[a-zA-Z0-9_]+]] = OpTypeImage %float Cube 2 1 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler + +// CHECK-DAG: %[[RA_Cube:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[CubeType]]{{$}} +// CHECK-DAG: %[[RA_CubeArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[CubeArrType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +RWByteAddressBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + TextureCube cube = ResourceDescriptorHeap[0]; + // CHECK: %[[CubeDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Cube]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[CubeHandle:[a-zA-Z0-9_]+]] = OpLoad %[[CubeType]] %[[CubeDesc]] + + TextureCubeArray cubeArr = ResourceDescriptorHeap[1]; + // CHECK: %[[CubeArrDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_CubeArr]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[CubeArrHandle:[a-zA-Z0-9_]+]] = OpLoad %[[CubeArrType]] %[[CubeArrDesc]] + + SamplerState samp = SamplerDescriptorHeap[0]; + // CHECK: %[[SampDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_0 + // CHECK: %[[SampHandle:[a-zA-Z0-9_]+]] = OpLoad %[[SamplerType]] %[[SampDesc]] + + float3 dir = normalize(float3(tid)); + + // CHECK: %[[CubeSI:[a-zA-Z0-9_]+]] = OpSampledImage %{{.*}} %[[CubeHandle]] %[[SampHandle]] + // CHECK: OpImageSampleExplicitLod %v4float %[[CubeSI]] + float4 v = cube.SampleLevel(samp, dir, 0); + + // CHECK: %[[CubeArrSI:[a-zA-Z0-9_]+]] = OpSampledImage %{{.*}} %[[CubeArrHandle]] %[[SampHandle]] + // CHECK: OpImageSampleExplicitLod %v4float %[[CubeArrSI]] + v += cubeArr.SampleLevel(samp, float4(dir, 0), 0); + + output.Store(0, asuint(v.x)); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl new file mode 100644 index 0000000000..4fb723c186 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl @@ -0,0 +1,59 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies: each HLSL resource element type lowers to the +// correct OpTypeImage format and sampled-vs-storage mode +// when accessed through the descriptor heap. +// +// Texture2D -> OpTypeImage %uint 2D ... Unknown -> sampled +// RWTexture2D -> OpTypeImage %float 2D ... Rg32f -> storage +// RWTexture2D -> OpTypeImage %uint 2D ... Rg32ui -> storage +// RWTexture2D -> OpTypeImage %int 2D ... R32i -> storage + +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant + +// CHECK-DAG: %[[TexUintType:[a-zA-Z0-9_]+]] = OpTypeImage %uint 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[RA_TexUint:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexUintType]]{{$}} + +// CHECK-DAG: %[[RWTexF2Type:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 2 Rg32f +// CHECK-DAG: %[[RA_RWTexF2:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexF2Type]]{{$}} + +// CHECK-DAG: %[[RWTexU2Type:[a-zA-Z0-9_]+]] = OpTypeImage %uint 2D 2 0 0 2 Rg32ui +// CHECK-DAG: %[[RA_RWTexU2:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexU2Type]]{{$}} + +// CHECK-DAG: %[[RWTexIType:[a-zA-Z0-9_]+]] = OpTypeImage %int 2D 2 0 0 2 R32i +// CHECK-DAG: %[[RA_RWTexI:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexIType]]{{$}} + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant + +RWByteAddressBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + Texture2D texUint = ResourceDescriptorHeap[0]; + // CHECK: %[[TexUintChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_TexUint]] %[[ResourceHeap]] %uint_0 + // CHECK: %[[TexUintH:[a-zA-Z0-9_]+]] = OpLoad %[[TexUintType]] %[[TexUintChain]] + + RWTexture2D rwTexF2 = ResourceDescriptorHeap[1]; + // CHECK: %[[RWTexF2Chain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RWTexF2]] %[[ResourceHeap]] %uint_1 + // CHECK: %[[RWTexF2H:[a-zA-Z0-9_]+]] = OpLoad %[[RWTexF2Type]] %[[RWTexF2Chain]] + + RWTexture2D rwTexU2 = ResourceDescriptorHeap[2]; + // CHECK: %[[RWTexU2Chain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RWTexU2]] %[[ResourceHeap]] %uint_2 + // CHECK: %[[RWTexU2H:[a-zA-Z0-9_]+]] = OpLoad %[[RWTexU2Type]] %[[RWTexU2Chain]] + + RWTexture2D rwTexI = ResourceDescriptorHeap[3]; + // CHECK: %[[RWTexIChain:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RWTexI]] %[[ResourceHeap]] %uint_3 + // CHECK: %[[RWTexIH:[a-zA-Z0-9_]+]] = OpLoad %[[RWTexIType]] %[[RWTexIChain]] + + // CHECK: OpImageFetch %v4uint %[[TexUintH]] + uint val = texUint.Load(int3(tid.xy, 0)).x; + + // CHECK: OpImageWrite %[[RWTexF2H]] + rwTexF2[tid.xy] = float2(val, val); + // CHECK: OpImageWrite %[[RWTexU2H]] + rwTexU2[tid.xy] = uint2(val, val); + // CHECK: OpImageWrite %[[RWTexIH]] + rwTexI[tid.xy] = int(val); + + output.Store(0, val); +} diff --git a/tools/clang/unittests/SPIRV/SpirvContextTest.cpp b/tools/clang/unittests/SPIRV/SpirvContextTest.cpp index a0f7a4b4c3..08d33e4892 100644 --- a/tools/clang/unittests/SPIRV/SpirvContextTest.cpp +++ b/tools/clang/unittests/SPIRV/SpirvContextTest.cpp @@ -324,6 +324,9 @@ TEST_F(SpirvContextTest, RuntimeArrayTypeUnique3) { EXPECT_NE(spvContext.getRuntimeArrayType(int32, 4), spvContext.getRuntimeArrayType(int32, llvm::None)); + + EXPECT_NE(spvContext.getRuntimeArrayType(int32, llvm::None), + spvContext.getRuntimeArrayType(int32, 32)); } TEST_F(SpirvContextTest, PointerTypeUnique1) { From c89e0222ac30be7f7a519918240f198c43a4e243 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 26 Jun 2026 14:22:21 -0700 Subject: [PATCH 02/45] Added OpConstantSizeOfEXT --- .../clang/include/clang/SPIRV/SpirvBuilder.h | 9 +++ .../clang/include/clang/SPIRV/SpirvContext.h | 6 +- .../include/clang/SPIRV/SpirvInstruction.h | 28 +++++++ tools/clang/include/clang/SPIRV/SpirvType.h | 14 +++- .../clang/include/clang/SPIRV/SpirvVisitor.h | 1 + tools/clang/lib/SPIRV/EmitVisitor.cpp | 33 +++++++- tools/clang/lib/SPIRV/EmitVisitor.h | 2 + tools/clang/lib/SPIRV/SpirvBuilder.cpp | 15 ++++ tools/clang/lib/SPIRV/SpirvContext.cpp | 7 +- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 21 ++---- tools/clang/lib/SPIRV/SpirvEmitter.h | 9 +-- tools/clang/lib/SPIRV/SpirvInstruction.cpp | 18 +++++ tools/clang/lib/SPIRV/SpirvType.cpp | 3 +- ...sm6_6.descriptorheap.ext.array-stride.hlsl | 75 +++++++++++++++++++ ...6_6.descriptorheap.ext.rwtexture-dims.hlsl | 10 +++ ...sm6_6.descriptorheap.ext.texture-dims.hlsl | 10 +++ .../sm6_6.descriptorheap.ext.texture-ms.hlsl | 6 ++ .../sm6_6.descriptorheap.ext.texture.hlsl | 6 ++ .../sm6_6.descriptorheap.ext.texturecube.hlsl | 8 ++ ...m6_6.descriptorheap.ext.typed-formats.hlsl | 10 +++ 20 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 71c62c427b..79b80bccda 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -831,6 +831,11 @@ class SpirvBuilder { bool specConst = false); SpirvConstant *getConstantNull(QualType); SpirvConstant *getConstantString(llvm::StringRef str, bool specConst = false); + /// \brief Returns the OpConstantSizeOfEXT (SPV_EXT_descriptor_heap) for the + /// given descriptor operandType, yielding its client-API size in bytes as + /// a 32-bit unsigned value. The result is cached per operand type, so each + /// descriptor type emits at most one instruction. + SpirvConstant *getConstantSizeOfEXT(const SpirvType *operandType); SpirvUndef *getUndef(QualType); SpirvString *createString(llvm::StringRef str); @@ -947,6 +952,10 @@ class SpirvBuilder { /// Used as caches for all created builtin variables to avoid duplication. llvm::SmallVector builtinVars; + /// Cache of OpConstantSizeOfEXT instructions keyed on the descriptor operand + /// type, so each distinct descriptor type emits at most one instruction. + llvm::DenseMap constantSizeOfEXTMap; + SpirvDebugInfoNone *debugNone; /// DebugExpression that does not reference any DebugOperation diff --git a/tools/clang/include/clang/SPIRV/SpirvContext.h b/tools/clang/include/clang/SPIRV/SpirvContext.h index 8a49f961ca..93c4b150cf 100644 --- a/tools/clang/include/clang/SPIRV/SpirvContext.h +++ b/tools/clang/include/clang/SPIRV/SpirvContext.h @@ -89,7 +89,8 @@ struct RuntimeArrayTypeMapInfo { static inline RuntimeArrayType *getTombstoneKey() { return nullptr; } static unsigned getHashValue(const RuntimeArrayType *Val) { return llvm::hash_combine(Val->getElementType(), - Val->getStride().hasValue()); + Val->getStride().hasValue(), + Val->getArrayStrideId()); } static bool isEqual(const RuntimeArrayType *LHS, const RuntimeArrayType *RHS) { @@ -284,7 +285,8 @@ class SpirvContext { llvm::Optional arrayStride); const RuntimeArrayType * getRuntimeArrayType(const SpirvType *elemType, - llvm::Optional arrayStride); + llvm::Optional arrayStride, + SpirvInstruction *arrayStrideId = nullptr); const NodePayloadArrayType * getNodePayloadArrayType(const SpirvType *elemType, const ParmVarDecl *nodeDecl); diff --git a/tools/clang/include/clang/SPIRV/SpirvInstruction.h b/tools/clang/include/clang/SPIRV/SpirvInstruction.h index d83d8f7294..ffc1a781b4 100644 --- a/tools/clang/include/clang/SPIRV/SpirvInstruction.h +++ b/tools/clang/include/clang/SPIRV/SpirvInstruction.h @@ -69,6 +69,7 @@ class SpirvInstruction { IK_ConstantFloat, IK_ConstantComposite, IK_ConstantString, + IK_ConstantSizeOfEXT, IK_ConstantNull, // Pointer <-> uint conversions. @@ -1537,6 +1538,33 @@ class SpirvConstantNull : public SpirvConstant { bool operator==(const SpirvConstantNull &that) const; }; +/// \brief Represents OpConstantSizeOfEXT (SPV_EXT_descriptor_heap). +/// +/// Yields the client-API-defined size of a descriptor type in bytes. Unlike +/// other constants its operand is a SPIR-V type (a descriptor type such as +/// OpTypeBufferEXT/OpTypeImage/OpTypeSampler), not a value. Used as the operand +/// of an ArrayStrideIdEXT decoration on descriptor-heap runtime arrays. +class SpirvConstantSizeOfEXT : public SpirvConstant { +public: + SpirvConstantSizeOfEXT(QualType resultType, const SpirvType *operandType); + + DEFINE_RELEASE_MEMORY_FOR_CLASS(SpirvConstantSizeOfEXT) + + // For LLVM-style RTTI + static bool classof(const SpirvInstruction *inst) { + return inst->getKind() == IK_ConstantSizeOfEXT; + } + + bool invokeVisitor(Visitor *v) override; + + bool operator==(const SpirvConstantSizeOfEXT &that) const; + + const SpirvType *getOperandType() const { return operandType; } + +private: + const SpirvType *operandType; +}; + class SpirvConstantString : public SpirvConstant { public: SpirvConstantString(llvm::StringRef stringLiteral, bool isSpecConst = false); diff --git a/tools/clang/include/clang/SPIRV/SpirvType.h b/tools/clang/include/clang/SPIRV/SpirvType.h index 654dad509f..6c71d4e1fd 100644 --- a/tools/clang/include/clang/SPIRV/SpirvType.h +++ b/tools/clang/include/clang/SPIRV/SpirvType.h @@ -23,6 +23,7 @@ namespace clang { namespace spirv { class HybridType; +class SpirvInstruction; enum class StructInterfaceType : uint32_t { InternalStorage = 0, @@ -274,8 +275,12 @@ class ArrayType : public SpirvType { class RuntimeArrayType : public SpirvType { public: RuntimeArrayType(const SpirvType *elemType, - llvm::Optional arrayStride) - : SpirvType(TK_RuntimeArray), elementType(elemType), stride(arrayStride) { + llvm::Optional arrayStride, + SpirvInstruction *strideId = nullptr) + : SpirvType(TK_RuntimeArray), elementType(elemType), stride(arrayStride), + arrayStrideId(strideId) { + assert(!(arrayStride.hasValue() && strideId) && + "literal stride and stride-id are mutually exclusive"); } static bool classof(const SpirvType *t) { @@ -286,12 +291,17 @@ class RuntimeArrayType : public SpirvType { const SpirvType *getElementType() const { return elementType; } llvm::Optional getStride() const { return stride; } + // When non-null, the array is decorated with ArrayStrideIdEXT referencing + // this constant (SPV_EXT_descriptor_heap) instead of a literal + // ArrayStride. Mutually exclusive with a literal stride. + SpirvInstruction *getArrayStrideId() const { return arrayStrideId; } private: const SpirvType *elementType; // Two runtime arrays with different ArrayStride decorations, are in fact two // different types. If no layout information is needed, use llvm::None. llvm::Optional stride; + SpirvInstruction *arrayStrideId = nullptr; }; class NodePayloadArrayType : public SpirvType { diff --git a/tools/clang/include/clang/SPIRV/SpirvVisitor.h b/tools/clang/include/clang/SPIRV/SpirvVisitor.h index 894f86ada2..fa2d1009ab 100644 --- a/tools/clang/include/clang/SPIRV/SpirvVisitor.h +++ b/tools/clang/include/clang/SPIRV/SpirvVisitor.h @@ -97,6 +97,7 @@ class Visitor { DEFINE_VISIT_METHOD(SpirvConstantFloat) DEFINE_VISIT_METHOD(SpirvConstantComposite) DEFINE_VISIT_METHOD(SpirvConstantString) + DEFINE_VISIT_METHOD(SpirvConstantSizeOfEXT) DEFINE_VISIT_METHOD(SpirvConstantNull) DEFINE_VISIT_METHOD(SpirvConvertPtrToU) DEFINE_VISIT_METHOD(SpirvConvertUToPtr) diff --git a/tools/clang/lib/SPIRV/EmitVisitor.cpp b/tools/clang/lib/SPIRV/EmitVisitor.cpp index 75d081ee62..618c1083b4 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.cpp +++ b/tools/clang/lib/SPIRV/EmitVisitor.cpp @@ -1132,6 +1132,13 @@ bool EmitVisitor::visit(SpirvConstantString *inst) { return true; } +bool EmitVisitor::visit(SpirvConstantSizeOfEXT *inst) { + typeHandler.emitConstantSizeOfEXT(inst); + emitDebugNameForInstruction(getOrAssignResultId(inst), + inst->getDebugName()); + return true; +} + bool EmitVisitor::visit(SpirvConstantNull *inst) { typeHandler.getOrCreateConstant(inst); emitDebugNameForInstruction(getOrAssignResultId(inst), @@ -2195,6 +2202,8 @@ uint32_t EmitTypeHandler::getOrCreateConstant(SpirvConstant *inst) { return getOrCreateConstantBool(constBool); } else if (auto *constString = dyn_cast(inst)) { return getOrCreateConstantString(constString); + } else if (auto *constSizeOf = dyn_cast(inst)) { + return emitConstantSizeOfEXT(constSizeOf); } else if (auto *constUndef = dyn_cast(inst)) { return getOrCreateUndef(constUndef); } @@ -2287,6 +2296,20 @@ uint32_t EmitTypeHandler::getOrCreateConstantNull(SpirvConstantNull *inst) { return inst->getResultId(); } +uint32_t EmitTypeHandler::emitConstantSizeOfEXT(SpirvConstantSizeOfEXT *inst) { + // Uniqueness is already guaranteed upstream by + // SpirvBuilder::getConstantSizeOfEXT (one instruction per descriptor type), + // so there is no emit-time cache to consult here. + const uint32_t typeId = emitType(inst->getResultType()); + const uint32_t operandTypeId = emitType(inst->getOperandType()); + initTypeInstruction(spv::Op::OpConstantSizeOfEXT); + curTypeInst.push_back(typeId); + curTypeInst.push_back(getOrAssignResultId(inst)); + curTypeInst.push_back(operandTypeId); + finalizeTypeInstruction(); + return inst->getResultId(); +} + uint32_t EmitTypeHandler::getOrCreateUndef(SpirvUndef *inst) { auto canonicalType = inst->getAstResultType().getCanonicalType(); auto found = std::find_if( @@ -2695,9 +2718,15 @@ uint32_t EmitTypeHandler::emitType(const SpirvType *type) { curTypeInst.push_back(elemTypeId); finalizeTypeInstruction(); - auto stride = raType->getStride(); - if (stride.hasValue()) + if (auto *strideId = raType->getArrayStrideId()) { + // SPV_EXT_descriptor_heap: stride given by a constant rather than a + // literal, emitted as OpDecorateId ... ArrayStrideIdEXT . + emitDecoration(id, spv::Decoration::ArrayStrideIdEXT, + {getOrAssignResultId(strideId)}, + llvm::None, /*usesIdParams=*/true); + } else if (auto stride = raType->getStride()) { emitDecoration(id, spv::Decoration::ArrayStride, {stride.getValue()}); + } } // NodePayloadArray types else if (const auto *npaType = dyn_cast(type)) { diff --git a/tools/clang/lib/SPIRV/EmitVisitor.h b/tools/clang/lib/SPIRV/EmitVisitor.h index d123243448..a859f6a651 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.h +++ b/tools/clang/lib/SPIRV/EmitVisitor.h @@ -109,6 +109,7 @@ class EmitTypeHandler { uint32_t getOrCreateConstantFloat(SpirvConstantFloat *); uint32_t getOrCreateConstantComposite(SpirvConstantComposite *); uint32_t getOrCreateConstantNull(SpirvConstantNull *); + uint32_t emitConstantSizeOfEXT(SpirvConstantSizeOfEXT *); uint32_t getOrCreateUndef(SpirvUndef *); uint32_t getOrCreateConstantBool(SpirvConstantBoolean *); uint32_t getOrCreateConstantString(SpirvConstantString *); @@ -271,6 +272,7 @@ class EmitVisitor : public Visitor { bool visit(SpirvConstantFloat *) override; bool visit(SpirvConstantComposite *) override; bool visit(SpirvConstantString *) override; + bool visit(SpirvConstantSizeOfEXT *) override; bool visit(SpirvConstantNull *) override; bool visit(SpirvConvertPtrToU *) override; bool visit(SpirvConvertUToPtr *) override; diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index 68e50b6574..9e79361780 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2022,6 +2022,21 @@ SpirvConstant *SpirvBuilder::getConstantNull(QualType type) { return nullConst; } +SpirvConstant *SpirvBuilder::getConstantSizeOfEXT(const SpirvType *operandType) { + // Reuse the existing instruction for a given descriptor type; multiple heap + // accesses of the same element type share one OpConstantSizeOfEXT. + auto found = constantSizeOfEXTMap.find(operandType); + if (found != constantSizeOfEXTMap.end()) + return found->second; + + // size is a non-negative 32-bit unsigned value, though spec allows signed + auto *sizeOfConst = new (context) + SpirvConstantSizeOfEXT(astContext.UnsignedIntTy, operandType); + mod->addConstant(sizeOfConst); + constantSizeOfEXTMap[operandType] = sizeOfConst; + return sizeOfConst; +} + SpirvConstant *SpirvBuilder::getConstantString(llvm::StringRef str, bool specConst) { // We do not care about making unique constants at this point. diff --git a/tools/clang/lib/SPIRV/SpirvContext.cpp b/tools/clang/lib/SPIRV/SpirvContext.cpp index a3397d74b4..1439861e89 100644 --- a/tools/clang/lib/SPIRV/SpirvContext.cpp +++ b/tools/clang/lib/SPIRV/SpirvContext.cpp @@ -268,14 +268,15 @@ SpirvContext::getArrayType(const SpirvType *elemType, uint32_t elemCount, const RuntimeArrayType * SpirvContext::getRuntimeArrayType(const SpirvType *elemType, - llvm::Optional arrayStride) { - RuntimeArrayType type(elemType, arrayStride); + llvm::Optional arrayStride, + SpirvInstruction *arrayStrideId) { + RuntimeArrayType type(elemType, arrayStride, arrayStrideId); auto found = runtimeArrayTypes.find(&type); if (found != runtimeArrayTypes.end()) return *found; auto inserted = runtimeArrayTypes.insert( - new (this) RuntimeArrayType(elemType, arrayStride)); + new (this) RuntimeArrayType(elemType, arrayStride, arrayStrideId)); return *(inserted.first); } diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 3f46837341..55954335c9 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5297,9 +5297,8 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( ? spv::StorageClass::Uniform : spv::StorageClass::StorageBuffer; const auto *bufferDescriptorType = spvContext.getBufferEXTType(bufferExtSC); - // Buffer descriptors are always on the resource heap. - const auto *arrayType = getDescriptorHeapRuntimeArrayType( - bufferDescriptorType, /*onSamplerHeap=*/false); + const auto *arrayType = + getDescriptorHeapRuntimeArrayType(bufferDescriptorType); auto *untypedAccessChainPtr = spvBuilder.createUntypedAccessChainKHR( untypedUniformConstantType, arrayType, heapVar, index, baseExpr->getExprLoc()); @@ -7017,9 +7016,7 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, const SpirvType *handleType = lowerTypeVisitor.lowerType(resourceType, SpirvLayoutRule::Void, llvm::None, baseExpr->getExprLoc()); - // Images/samplers may come from either heap; pick the right stride. - const auto *arrayType = getDescriptorHeapRuntimeArrayType( - handleType, isSamplerDescriptorHeap(decl)); + const auto *arrayType = getDescriptorHeapRuntimeArrayType(handleType); auto *untypedAccessChainPtr = spvBuilder.createUntypedAccessChainKHR( untypedUniformConstantType, arrayType, var, index, baseExpr->getExprLoc()); @@ -9226,13 +9223,11 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { } const SpirvType * -SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType, - bool onSamplerHeap) { - constexpr uint32_t kDefaultResourceHeapStride = 64; - constexpr uint32_t kDefaultSamplerHeapStride = 32; - const uint32_t stride = - onSamplerHeap ? kDefaultSamplerHeapStride : kDefaultResourceHeapStride; - return spvContext.getRuntimeArrayType(elemType, stride); +SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType) { + // The stride is the client-API defined size of the element descriptor type, + // given by OpConstantSizeOfEXT and applied via ArrayStrideIdEXT decoration. + SpirvInstruction *sizeOf = spvBuilder.getConstantSizeOfEXT(elemType); + return spvContext.getRuntimeArrayType(elemType, llvm::None, sizeOf); } SpirvInstruction * diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 76c174eab4..9f206fcb17 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -399,11 +399,10 @@ class SpirvEmitter : public ASTConsumer { /// Translates the given varDecl into a spec constant. void createSpecConstant(const VarDecl *varDecl); - /// Returns the OpTypeRuntimeArray for a descriptor-heap array of elemType - /// decorated with the default ArrayStride (64 bytes for the resource heap, - /// 32 bytes for the sampler heap). - const SpirvType *getDescriptorHeapRuntimeArrayType(const SpirvType *elemType, - bool onSamplerHeap); + /// Returns the OpTypeRuntimeArray for a descriptor heap array of elemType, + /// decorated with ArrayStrideIdEXT referencing an OpConstantSizeOfEXT of the + /// element (descriptor) type. + const SpirvType *getDescriptorHeapRuntimeArrayType(const SpirvType *elemType); /// Emits the native (SPV_EXT_descriptor_heap) access for a buffer-like /// resource (StructuredBuffer/ByteAddressBuffer/ConstantBuffer/TextureBuffer diff --git a/tools/clang/lib/SPIRV/SpirvInstruction.cpp b/tools/clang/lib/SPIRV/SpirvInstruction.cpp index 28c3cfee17..342a3ec55a 100644 --- a/tools/clang/lib/SPIRV/SpirvInstruction.cpp +++ b/tools/clang/lib/SPIRV/SpirvInstruction.cpp @@ -67,6 +67,7 @@ DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConstantInteger) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConstantFloat) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConstantComposite) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConstantString) +DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConstantSizeOfEXT) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConstantNull) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConvertPtrToU) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvConvertUToPtr) @@ -627,6 +628,11 @@ bool SpirvConstant::operator==(const SpirvConstant &that) const { if (thatNullInst == nullptr) return false; return *nullInst == *thatNullInst; + } else if (auto *sizeOfInst = dyn_cast(this)) { + auto *thatSizeOfInst = dyn_cast(&that); + if (thatSizeOfInst == nullptr) + return false; + return *sizeOfInst == *thatSizeOfInst; } else if (auto *nullInst = dyn_cast(this)) { auto *thatNullInst = dyn_cast(&that); if (thatNullInst == nullptr) @@ -711,6 +717,18 @@ bool SpirvConstantString::operator==(const SpirvConstantString &that) const { str == that.str; } +SpirvConstantSizeOfEXT::SpirvConstantSizeOfEXT(QualType resultType, + const SpirvType *operandType) + : SpirvConstant(IK_ConstantSizeOfEXT, spv::Op::OpConstantSizeOfEXT, + resultType), + operandType(operandType) {} + +bool SpirvConstantSizeOfEXT::operator==( + const SpirvConstantSizeOfEXT &that) const { + return resultType == that.resultType && + astResultType == that.astResultType && operandType == that.operandType; +} + SpirvConstantNull::SpirvConstantNull(QualType type) : SpirvConstant(IK_ConstantNull, spv::Op::OpConstantNull, type) {} diff --git a/tools/clang/lib/SPIRV/SpirvType.cpp b/tools/clang/lib/SPIRV/SpirvType.cpp index caa4f6d52b..e43c6fe9ae 100644 --- a/tools/clang/lib/SPIRV/SpirvType.cpp +++ b/tools/clang/lib/SPIRV/SpirvType.cpp @@ -169,7 +169,8 @@ bool ArrayType::operator==(const ArrayType &that) const { bool RuntimeArrayType::operator==(const RuntimeArrayType &that) const { return elementType == that.elementType && stride.hasValue() == that.stride.hasValue() && - (!stride.hasValue() || stride.getValue() == that.stride.getValue()); + (!stride.hasValue() || stride.getValue() == that.stride.getValue()) && + arrayStrideId == that.arrayStrideId; } bool NodePayloadArrayType::operator==(const NodePayloadArrayType &that) const { diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl new file mode 100644 index 0000000000..29758fbd8e --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl @@ -0,0 +1,75 @@ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s --check-prefix=DEDUP + +// Verifies the default descriptor-heap array stride. +// +// With no manual intervention, every heap runtime array must be decorated with ArrayStrideIdEXT +// referencing an OpConstantSizeOfEXT of the array's element (descriptor) type: +// +// StructuredBuffer / RWStructuredBuffer / ByteAddressBuffer -> OpTypeBufferEXT StorageBuffer +// ConstantBuffer -> OpTypeBufferEXT Uniform +// Texture2D -> OpTypeImage +// SamplerState -> OpTypeSampler +// +// The OpConstantSizeOfEXT result type is %uint + +// CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant + +// Element (descriptor) types. +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[UBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform +// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D +// CHECK-DAG: %[[SamplerDesc:[a-zA-Z0-9_]+]] = OpTypeSampler + +// Heap runtime arrays of those element types. +// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] +// CHECK-DAG: %[[UBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[UBufDesc]] +// CHECK-DAG: %[[TexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexDesc]] +// CHECK-DAG: %[[SamplerArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerDesc]] + +// One OpConstantSizeOfEXT per distinct element type, result type %uint. +// CHECK-DAG: %[[SBBufSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SBBufDesc]] +// CHECK-DAG: %[[UBufSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBufDesc]] +// CHECK-DAG: %[[TexSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[TexDesc]] +// CHECK-DAG: %[[SamplerSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SamplerDesc]] + +// Each heap array is decorated ArrayStrideIdEXT with its element's sizeof. +// CHECK-DAG: OpDecorateId %[[SBBufArray]] ArrayStrideIdEXT %[[SBBufSize]] +// CHECK-DAG: OpDecorateId %[[UBufArray]] ArrayStrideIdEXT %[[UBufSize]] +// CHECK-DAG: OpDecorateId %[[TexArray]] ArrayStrideIdEXT %[[TexSize]] +// CHECK-DAG: OpDecorateId %[[SamplerArray]] ArrayStrideIdEXT %[[SamplerSize]] + +// No literal default stride decoration on any heap array. +// CHECK-NOT: OpDecorate %{{.*}} ArrayStride 64 +// CHECK-NOT: OpDecorate %{{.*}} ArrayStride 32 + +// Dedup: StructuredBuffer and ByteAddressBuffer below both lower to the same +// OpTypeBufferEXT StorageBuffer descriptor, so its OpConstantSizeOfEXT must be +// emitted exactly ONCE and shared, not one per heap access. + +// Separate prefix: annotations precede the constants section in SPIR-V, so a +// trailing ordered check in the main pass would search past the sizeof. +// {{$}} anchors the match: "StorageBuffer" is a prefix of "StorageBuffer_0" +// (the Uniform descriptor name), so without it DEDUP-NOT would false-fire. + +// DEDUP: %[[SBDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// DEDUP-COUNT-1: OpConstantSizeOfEXT %uint %[[SBDesc]]{{$}} +// DEDUP-NOT: OpConstantSizeOfEXT %uint %[[SBDesc]]{{$}} + +struct Constants { + uint value; +}; + +RWStructuredBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + StructuredBuffer sbuf = ResourceDescriptorHeap[0]; + ByteAddressBuffer babuf = ResourceDescriptorHeap[1]; + ConstantBuffer cbuf = ResourceDescriptorHeap[2]; + Texture2D tex = ResourceDescriptorHeap[3]; + SamplerState samp = SamplerDescriptorHeap[0]; + + float4 color = tex.SampleLevel(samp, float2(0, 0), 0); + output[tid.x] = color + sbuf.Load(tid.x) + babuf.Load(tid.x * 4) + cbuf.value; +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl index 87d2591318..37d45993a7 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl @@ -19,6 +19,16 @@ // CHECK-DAG: %[[RA_RW2DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW2DArrType]]{{$}} // CHECK-DAG: %[[RA_RW3D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW3DType]]{{$}} +// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. +// CHECK-DAG: %[[RW1DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW1DType]] +// CHECK-DAG: %[[RW1DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW1DArrType]] +// CHECK-DAG: %[[RW2DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW2DArrType]] +// CHECK-DAG: %[[RW3DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW3DType]] +// CHECK-DAG: OpDecorateId %[[RA_RW1D]] ArrayStrideIdEXT %[[RW1DSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW1DArr]] ArrayStrideIdEXT %[[RW1DArrSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW2DArr]] ArrayStrideIdEXT %[[RW2DArrSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW3D]] ArrayStrideIdEXT %[[RW3DSize]] + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant [numthreads(1, 1, 1)] diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl index 120830aa4c..274bcd11bc 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl @@ -19,6 +19,16 @@ // CHECK-DAG: %[[RA_Tex2DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DArrType]]{{$}} // CHECK-DAG: %[[RA_Tex3D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex3DType]]{{$}} +// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. +// CHECK-DAG: %[[Tex1DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex1DType]] +// CHECK-DAG: %[[Tex1DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex1DArrType]] +// CHECK-DAG: %[[Tex2DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex2DArrType]] +// CHECK-DAG: %[[Tex3DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex3DType]] +// CHECK-DAG: OpDecorateId %[[RA_Tex1D]] ArrayStrideIdEXT %[[Tex1DSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex1DArr]] ArrayStrideIdEXT %[[Tex1DArrSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex2DArr]] ArrayStrideIdEXT %[[Tex2DArrSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex3D]] ArrayStrideIdEXT %[[Tex3DSize]] + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant RWByteAddressBuffer output : register(u0); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl index edb2d78821..cd7e2db43f 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl @@ -10,6 +10,12 @@ // CHECK-DAG: %[[RA_MS:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[MSType]]{{$}} // CHECK-DAG: %[[RA_MSArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[MSArrType]]{{$}} +// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. +// CHECK-DAG: %[[MSSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[MSType]] +// CHECK-DAG: %[[MSArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[MSArrType]] +// CHECK-DAG: OpDecorateId %[[RA_MS]] ArrayStrideIdEXT %[[MSSize]] +// CHECK-DAG: OpDecorateId %[[RA_MSArr]] ArrayStrideIdEXT %[[MSArrSize]] + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant RWByteAddressBuffer output : register(u0); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl index 6d769383f6..84c15a2869 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl @@ -14,6 +14,12 @@ // CHECK-DAG: %[[RA_BufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[BufferType]]{{$}} // CHECK-DAG: %[[RA_RWBufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWBufferType]]{{$}} +// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. +// CHECK-DAG: %[[BufferSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[BufferType]] +// CHECK-DAG: %[[RWBufferSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWBufferType]] +// CHECK-DAG: OpDecorateId %[[RA_BufferType]] ArrayStrideIdEXT %[[BufferSize]] +// CHECK-DAG: OpDecorateId %[[RA_RWBufferType]] ArrayStrideIdEXT %[[RWBufferSize]] + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant [numthreads(1, 1, 1)] diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl index 237fed4aa9..755b4adefb 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl @@ -13,6 +13,14 @@ // CHECK-DAG: %[[RA_CubeArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[CubeArrType]]{{$}} // CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} +// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. +// CHECK-DAG: %[[CubeSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[CubeType]] +// CHECK-DAG: %[[CubeArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[CubeArrType]] +// CHECK-DAG: %[[SamplerSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SamplerType]] +// CHECK-DAG: OpDecorateId %[[RA_Cube]] ArrayStrideIdEXT %[[CubeSize]] +// CHECK-DAG: OpDecorateId %[[RA_CubeArr]] ArrayStrideIdEXT %[[CubeArrSize]] +// CHECK-DAG: OpDecorateId %[[RA_Sampler]] ArrayStrideIdEXT %[[SamplerSize]] + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant // CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl index 4fb723c186..d001691ced 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl @@ -23,6 +23,16 @@ // CHECK-DAG: %[[RWTexIType:[a-zA-Z0-9_]+]] = OpTypeImage %int 2D 2 0 0 2 R32i // CHECK-DAG: %[[RA_RWTexI:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexIType]]{{$}} +// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. +// CHECK-DAG: %[[TexUintSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[TexUintType]] +// CHECK-DAG: %[[RWTexF2Size:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWTexF2Type]] +// CHECK-DAG: %[[RWTexU2Size:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWTexU2Type]] +// CHECK-DAG: %[[RWTexISize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWTexIType]] +// CHECK-DAG: OpDecorateId %[[RA_TexUint]] ArrayStrideIdEXT %[[TexUintSize]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexF2]] ArrayStrideIdEXT %[[RWTexF2Size]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexU2]] ArrayStrideIdEXT %[[RWTexU2Size]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexI]] ArrayStrideIdEXT %[[RWTexISize]] + // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant RWByteAddressBuffer output : register(u0); From 002e1ad660c5c2f47c2fdc12d399ec7acd2c459a Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 6 Jul 2026 12:23:21 -0700 Subject: [PATCH 03/45] clang-format --- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 3 ++- tools/clang/lib/SPIRV/SpirvInstruction.cpp | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index 9e79361780..5002c2cbd5 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2022,7 +2022,8 @@ SpirvConstant *SpirvBuilder::getConstantNull(QualType type) { return nullConst; } -SpirvConstant *SpirvBuilder::getConstantSizeOfEXT(const SpirvType *operandType) { +SpirvConstant * +SpirvBuilder::getConstantSizeOfEXT(const SpirvType *operandType) { // Reuse the existing instruction for a given descriptor type; multiple heap // accesses of the same element type share one OpConstantSizeOfEXT. auto found = constantSizeOfEXTMap.find(operandType); diff --git a/tools/clang/lib/SPIRV/SpirvInstruction.cpp b/tools/clang/lib/SPIRV/SpirvInstruction.cpp index 342a3ec55a..24b433c611 100644 --- a/tools/clang/lib/SPIRV/SpirvInstruction.cpp +++ b/tools/clang/lib/SPIRV/SpirvInstruction.cpp @@ -725,8 +725,8 @@ SpirvConstantSizeOfEXT::SpirvConstantSizeOfEXT(QualType resultType, bool SpirvConstantSizeOfEXT::operator==( const SpirvConstantSizeOfEXT &that) const { - return resultType == that.resultType && - astResultType == that.astResultType && operandType == that.operandType; + return resultType == that.resultType && astResultType == that.astResultType && + operandType == that.operandType; } SpirvConstantNull::SpirvConstantNull(QualType type) From 2e1c7fbea0e5e0d3c872d92b10382850fe0880f1 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 9 Jul 2026 18:10:39 -0700 Subject: [PATCH 04/45] Shared max(image,buffer) stride for descriptor heap resource arrays --- .../clang/include/clang/SPIRV/SpirvBuilder.h | 20 +++++ .../include/clang/SPIRV/SpirvInstruction.h | 30 +++++++ .../clang/include/clang/SPIRV/SpirvVisitor.h | 1 + tools/clang/lib/SPIRV/EmitVisitor.cpp | 14 ++++ tools/clang/lib/SPIRV/EmitVisitor.h | 1 + tools/clang/lib/SPIRV/SpirvBuilder.cpp | 54 ++++++++++++ tools/clang/lib/SPIRV/SpirvEmitter.cpp | 74 +++++++++------- tools/clang/lib/SPIRV/SpirvInstruction.cpp | 8 ++ ...sm6_6.descriptorheap.ext.array-stride.hlsl | 84 ++++++++++--------- ...6_6.descriptorheap.ext.rwtexture-dims.hlsl | 15 ++-- ...sm6_6.descriptorheap.ext.texture-dims.hlsl | 15 ++-- .../sm6_6.descriptorheap.ext.texture-ms.hlsl | 9 +- .../sm6_6.descriptorheap.ext.texture.hlsl | 9 +- .../sm6_6.descriptorheap.ext.texturecube.hlsl | 12 ++- ...m6_6.descriptorheap.ext.typed-formats.hlsl | 15 ++-- 15 files changed, 247 insertions(+), 114 deletions(-) diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 79b80bccda..20b6ff2d9b 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -836,6 +836,20 @@ class SpirvBuilder { /// a 32-bit unsigned value. The result is cached per operand type, so each /// descriptor type emits at most one instruction. SpirvConstant *getConstantSizeOfEXT(const SpirvType *operandType); + + SpirvSpecConstantTernaryOp * + createSpecConstantTernaryOp(spv::Op op, QualType resultType, + SpirvInstruction *op1, SpirvInstruction *op2, + SpirvInstruction *op3, SourceLocation loc); + + /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays: + /// max(sizeof(image), sizeof(buffer)) computed via OpSpecConstantOp. + /// Cached per module. + SpirvInstruction *getResourceHeapArrayStride(); + + /// \brief Shared ArrayStrideIdEXT operand for sampler-heap runtime arrays: + /// the sampler descriptor size. Cached per module. + SpirvInstruction *getSamplerHeapArrayStride(); SpirvUndef *getUndef(QualType); SpirvString *createString(llvm::StringRef str); @@ -956,6 +970,12 @@ class SpirvBuilder { /// type, so each distinct descriptor type emits at most one instruction. llvm::DenseMap constantSizeOfEXTMap; + /// Cached shared descriptor-heap array strides (SPV_EXT_descriptor_heap), so + /// each is emitted once per module (see + /// get{Resource,Sampler}HeapArrayStride). + SpirvInstruction *resourceHeapArrayStride = nullptr; + SpirvInstruction *samplerHeapArrayStride = nullptr; + SpirvDebugInfoNone *debugNone; /// DebugExpression that does not reference any DebugOperation diff --git a/tools/clang/include/clang/SPIRV/SpirvInstruction.h b/tools/clang/include/clang/SPIRV/SpirvInstruction.h index ffc1a781b4..428464be31 100644 --- a/tools/clang/include/clang/SPIRV/SpirvInstruction.h +++ b/tools/clang/include/clang/SPIRV/SpirvInstruction.h @@ -138,6 +138,7 @@ class SpirvInstruction { IK_ReadClock, // OpReadClock IK_SampledImage, // OpSampledImage IK_Select, // OpSelect + IK_SpecConstantTernaryOp, // SpecConstant ternary operations IK_SpecConstantBinaryOp, // SpecConstant binary operations IK_SpecConstantUnaryOp, // SpecConstant unary operations IK_Store, // OpStore @@ -2255,6 +2256,35 @@ class SpirvSelect : public SpirvInstruction { SpirvInstruction *falseObject; }; +/// \brief OpSpecConstantOp instruction where the operation is ternary. +class SpirvSpecConstantTernaryOp : public SpirvInstruction { +public: + SpirvSpecConstantTernaryOp(spv::Op specConstantOp, QualType resultType, + SourceLocation loc, SpirvInstruction *operand1, + SpirvInstruction *operand2, + SpirvInstruction *operand3); + + DEFINE_RELEASE_MEMORY_FOR_CLASS(SpirvSpecConstantTernaryOp) + + // For LLVM-style RTTI + static bool classof(const SpirvInstruction *inst) { + return inst->getKind() == IK_SpecConstantTernaryOp; + } + + bool invokeVisitor(Visitor *v) override; + + spv::Op getSpecConstantopcode() const { return specOp; } + SpirvInstruction *getOperand1() const { return operand1; } + SpirvInstruction *getOperand2() const { return operand2; } + SpirvInstruction *getOperand3() const { return operand3; } + +private: + spv::Op specOp; + SpirvInstruction *operand1; + SpirvInstruction *operand2; + SpirvInstruction *operand3; +}; + /// \brief OpSpecConstantOp instruction where the operation is binary. class SpirvSpecConstantBinaryOp : public SpirvInstruction { public: diff --git a/tools/clang/include/clang/SPIRV/SpirvVisitor.h b/tools/clang/include/clang/SPIRV/SpirvVisitor.h index fa2d1009ab..5dd37bc4db 100644 --- a/tools/clang/include/clang/SPIRV/SpirvVisitor.h +++ b/tools/clang/include/clang/SPIRV/SpirvVisitor.h @@ -119,6 +119,7 @@ class Visitor { DEFINE_VISIT_METHOD(SpirvCopyObject) DEFINE_VISIT_METHOD(SpirvSampledImage) DEFINE_VISIT_METHOD(SpirvSelect) + DEFINE_VISIT_METHOD(SpirvSpecConstantTernaryOp) DEFINE_VISIT_METHOD(SpirvSpecConstantBinaryOp) DEFINE_VISIT_METHOD(SpirvSpecConstantUnaryOp) DEFINE_VISIT_METHOD(SpirvStore) diff --git a/tools/clang/lib/SPIRV/EmitVisitor.cpp b/tools/clang/lib/SPIRV/EmitVisitor.cpp index 618c1083b4..6cf8450f34 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.cpp +++ b/tools/clang/lib/SPIRV/EmitVisitor.cpp @@ -1430,6 +1430,20 @@ bool EmitVisitor::visit(SpirvSelect *inst) { return true; } +bool EmitVisitor::visit(SpirvSpecConstantTernaryOp *inst) { + initInstruction(inst); + curInst.push_back(inst->getResultTypeId()); + curInst.push_back(getOrAssignResultId(inst)); + curInst.push_back(static_cast(inst->getSpecConstantopcode())); + curInst.push_back(getOrAssignResultId(inst->getOperand1())); + curInst.push_back(getOrAssignResultId(inst->getOperand2())); + curInst.push_back(getOrAssignResultId(inst->getOperand3())); + finalizeInstruction(&typeConstantBinary); + emitDebugNameForInstruction(getOrAssignResultId(inst), + inst->getDebugName()); + return true; +} + bool EmitVisitor::visit(SpirvSpecConstantBinaryOp *inst) { initInstruction(inst); curInst.push_back(inst->getResultTypeId()); diff --git a/tools/clang/lib/SPIRV/EmitVisitor.h b/tools/clang/lib/SPIRV/EmitVisitor.h index a859f6a651..d3c97c6281 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.h +++ b/tools/clang/lib/SPIRV/EmitVisitor.h @@ -291,6 +291,7 @@ class EmitVisitor : public Visitor { bool visit(SpirvCopyObject *) override; bool visit(SpirvSampledImage *) override; bool visit(SpirvSelect *) override; + bool visit(SpirvSpecConstantTernaryOp *) override; bool visit(SpirvSpecConstantBinaryOp *) override; bool visit(SpirvSpecConstantUnaryOp *) override; bool visit(SpirvStore *) override; diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index 5002c2cbd5..4478aa27c0 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -471,6 +471,16 @@ SpirvSpecConstantBinaryOp *SpirvBuilder::createSpecConstantBinaryOp( return instruction; } +SpirvSpecConstantTernaryOp *SpirvBuilder::createSpecConstantTernaryOp( + spv::Op op, QualType resultType, SpirvInstruction *op1, + SpirvInstruction *op2, SpirvInstruction *op3, SourceLocation loc) { + assert(insertPoint && "null insert point"); + auto *instruction = new (context) + SpirvSpecConstantTernaryOp(op, resultType, loc, op1, op2, op3); + insertPoint->addInstruction(instruction); + return instruction; +} + SpirvGroupNonUniformOp *SpirvBuilder::createGroupNonUniformOp( spv::Op op, QualType resultType, llvm::Optional execScope, llvm::ArrayRef operands, SourceLocation loc, @@ -2038,6 +2048,50 @@ SpirvBuilder::getConstantSizeOfEXT(const SpirvType *operandType) { return sizeOfConst; } +SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { + if (resourceHeapArrayStride) + return resourceHeapArrayStride; + + // The HLSL SM6.6 ResourceDescriptorHeap is a single flat array in which the + // client may place any resource descriptor at any slot. To match DX12 + // semantics, all resource descriptor arrays must share one stride equal to + // the largest resource descriptor size: max(sizeof(image), sizeof(buffer)). + // Images and buffers are the two resource descriptor categories defined by + // VkPhysicalDeviceDescriptorHeapPropertiesEXT (imageDescriptorSize / + // bufferDescriptorSize); textures lower to OpTypeImage, so image/buffer + // covers all relevant HLSL resource kinds. + // Both sizes are driver defined and known only at pipeline creation time, so + // the maximum is computed with OpSpecConstantOp over two OpConstantSizeOfEXT + // placeholders. A canonical sampled 2D float image and a Uniform buffer stand + // in as representatives; VkPhysicalDeviceDescriptorHeapPropertiesEXT reports + // one size per category, so subtype and storage class do not affect the size. + const SpirvType *placeholderImage = context.getImageType( + context.getFloatType(32), spv::Dim::Dim2D, ImageType::WithDepth::No, + /*arrayed*/ false, /*ms*/ false, ImageType::WithSampler::Yes, + spv::ImageFormat::Unknown); + const SpirvType *placeholderBuffer = + context.getBufferEXTType(spv::StorageClass::Uniform); + + SpirvInstruction *imageSize = getConstantSizeOfEXT(placeholderImage); + SpirvInstruction *bufferSize = getConstantSizeOfEXT(placeholderBuffer); + SpirvInstruction *imageIsBigger = createSpecConstantBinaryOp( + spv::Op::OpUGreaterThan, astContext.BoolTy, imageSize, bufferSize, {}); + resourceHeapArrayStride = + createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, + imageIsBigger, imageSize, bufferSize, {}); + return resourceHeapArrayStride; +} + +SpirvInstruction *SpirvBuilder::getSamplerHeapArrayStride() { + if (samplerHeapArrayStride) + return samplerHeapArrayStride; + // The sampler heap holds only OpTypeSampler descriptors. All samplers are the + // same size (unlike resources, which split into image and buffer categories + // that may differ), so the stride is just that one descriptor size. + samplerHeapArrayStride = getConstantSizeOfEXT(context.getSamplerType()); + return samplerHeapArrayStride; +} + SpirvConstant *SpirvBuilder::getConstantString(llvm::StringRef str, bool specConst) { // We do not care about making unique constants at this point. diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 55954335c9..d979918802 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5122,9 +5122,10 @@ bool SpirvEmitter::isDescriptorHeapCounterUnsupported(const Expr *expr) const { SpirvInstruction *SpirvEmitter::emitDescriptorHeapAccessChain( const SpirvType *arrayType, SpirvInstruction *heap, SpirvVariable *indexVar, SourceLocation loc) { - const auto *untypedUniformConstantType = + const UntypedPointerKHRType *untypedUniformConstantType = spvContext.getUntypedPointerKHRType(spv::StorageClass::UniformConstant); - auto *index = spvBuilder.createLoad(astContext.UnsignedIntTy, indexVar, loc); + SpirvInstruction *index = + spvBuilder.createLoad(astContext.UnsignedIntTy, indexVar, loc); return spvBuilder.createUntypedAccessChainKHR(untypedUniformConstantType, arrayType, heap, index, loc); } @@ -5222,9 +5223,9 @@ SpirvEmitter::emitDescriptorHeapBufferPointer(const VarDecl *decl, if (found == descriptorHeapBufferAliasVars.end()) return nullptr; - auto *descriptorPtr = emitDescriptorHeapAccessChain( + SpirvInstruction *descriptorPtr = emitDescriptorHeapAccessChain( found->second.arrayType, found->second.heap, found->second.indexVar, loc); - auto *bufferDataPtr = spvBuilder.createUnaryOp( + SpirvUnaryOp *bufferDataPtr = spvBuilder.createUnaryOp( spv::Op::OpBufferPointerEXT, found->second.bufferPointerType, descriptorPtr, loc); bufferDataPtr->setStorageClass( @@ -5241,11 +5242,12 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapImageTexelPointer( if (found == descriptorHeapImageAliasVars.end()) return nullptr; - auto *descriptorPtr = emitDescriptorHeapAccessChain( + SpirvInstruction *descriptorPtr = emitDescriptorHeapAccessChain( found->second.arrayType, found->second.heap, found->second.indexVar, loc); - auto *ptr = spvBuilder.createUntypedImageTexelPointerEXT( - resultType, found->second.imageType, descriptorPtr, coordinate, sample, - loc); + SpirvUntypedImageTexelPointerEXT *ptr = + spvBuilder.createUntypedImageTexelPointerEXT( + resultType, found->second.imageType, descriptorPtr, coordinate, + sample, loc); ptr->setStorageClass(spv::StorageClass::Image); return ptr; } @@ -5264,7 +5266,7 @@ getDescriptorHeapBufferStorageClass(QualType resourceType) { SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, const Expr *expr, const Expr *baseExpr, const Expr *indexExpr) { - const auto *untypedUniformConstantType = + const UntypedPointerKHRType *untypedUniformConstantType = spvContext.getUntypedPointerKHRType(spv::StorageClass::UniformConstant); LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, spvBuilder); @@ -5296,13 +5298,15 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( const spv::StorageClass bufferExtSC = isConstantBuffer(resourceType) ? spv::StorageClass::Uniform : spv::StorageClass::StorageBuffer; - const auto *bufferDescriptorType = spvContext.getBufferEXTType(bufferExtSC); - const auto *arrayType = + const BufferEXTType *bufferDescriptorType = + spvContext.getBufferEXTType(bufferExtSC); + const SpirvType *arrayType = getDescriptorHeapRuntimeArrayType(bufferDescriptorType); - auto *untypedAccessChainPtr = spvBuilder.createUntypedAccessChainKHR( - untypedUniformConstantType, arrayType, heapVar, index, - baseExpr->getExprLoc()); - auto *bufferDataPtr = spvBuilder.createUnaryOp( + SpirvUntypedAccessChainKHR *untypedAccessChainPtr = + spvBuilder.createUntypedAccessChainKHR(untypedUniformConstantType, + arrayType, heapVar, index, + baseExpr->getExprLoc()); + SpirvUnaryOp *bufferDataPtr = spvBuilder.createUnaryOp( spv::Op::OpBufferPointerEXT, bufferDataPointerType, untypedAccessChainPtr, baseExpr->getExprLoc()); bufferDataPtr->setStorageClass(bufferDataPointerType->getStorageClass()); @@ -6985,11 +6989,12 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, baseExpr->getExprLoc()); return nullptr; } - auto *var = declIdMapper.createResourceHeap(decl, resourceType); + SpirvVariableLike *var = + declIdMapper.createResourceHeap(decl, resourceType); if (hlsl::HasHLSLGloballyCoherent(resourceType)) spvBuilder.decorateCoherent(var, baseExpr->getExprLoc()); - auto *index = doExpr(indexExpr); + SpirvInstruction *index = doExpr(indexExpr); if (spirvOptions.useDescriptorHeap) { needsLegalization = true; @@ -7003,12 +7008,11 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, } if (isAKindOfStructuredOrByteBuffer(resourceType) || - isConstantTextureBuffer(resourceType)) { + isConstantTextureBuffer(resourceType)) return emitDescriptorHeapBufferAccess(resourceType, var, index, expr, baseExpr, indexExpr); - } - const auto *untypedUniformConstantType = + const UntypedPointerKHRType *untypedUniformConstantType = spvContext.getUntypedPointerKHRType( spv::StorageClass::UniformConstant); LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, @@ -7016,15 +7020,16 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, const SpirvType *handleType = lowerTypeVisitor.lowerType(resourceType, SpirvLayoutRule::Void, llvm::None, baseExpr->getExprLoc()); - const auto *arrayType = getDescriptorHeapRuntimeArrayType(handleType); - auto *untypedAccessChainPtr = spvBuilder.createUntypedAccessChainKHR( - untypedUniformConstantType, arrayType, var, index, - baseExpr->getExprLoc()); - if (isRasterizerOrderedView(resourceType)) { + const SpirvType *arrayType = + getDescriptorHeapRuntimeArrayType(handleType); + SpirvUntypedAccessChainKHR *untypedAccessChainPtr = + spvBuilder.createUntypedAccessChainKHR(untypedUniformConstantType, + arrayType, var, index, + baseExpr->getExprLoc()); + if (isRasterizerOrderedView(resourceType)) spvBuilder.addExecutionMode(entryFunction, declIdMapper.getInterlockExecutionMode(), {}, baseExpr->getExprLoc()); - } descriptorHeapImageAccesses[expr] = { untypedAccessChainPtr, handleType, arrayType, var, index, indexExpr->getType()}; @@ -7032,7 +7037,7 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, baseExpr->getExprLoc(), range); } - auto *accessChainPtr = spvBuilder.createAccessChain( + SpirvInstruction *accessChainPtr = spvBuilder.createAccessChain( resourceType, var, index, baseExpr->getExprLoc(), range); if (!isAKindOfStructuredOrByteBuffer(resourceType) && @@ -9224,10 +9229,17 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { const SpirvType * SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType) { - // The stride is the client-API defined size of the element descriptor type, - // given by OpConstantSizeOfEXT and applied via ArrayStrideIdEXT decoration. - SpirvInstruction *sizeOf = spvBuilder.getConstantSizeOfEXT(elemType); - return spvContext.getRuntimeArrayType(elemType, llvm::None, sizeOf); + // SPV_EXT_descriptor_heap: apply a client-API-defined byte stride via an + // ArrayStrideIdEXT decoration. The sampler heap holds a single descriptor + // type, so its stride is the sampler descriptor size. The resource heap is a + // shared flat array in which any resource descriptor may sit at any slot, so + // every resource runtime array must use one common stride: max(sizeof(image), + // sizeof(buffer)). Using the accessed element size would be wrong for the + // resource heap. + SpirvInstruction *strideId = isa(elemType) + ? spvBuilder.getSamplerHeapArrayStride() + : spvBuilder.getResourceHeapArrayStride(); + return spvContext.getRuntimeArrayType(elemType, llvm::None, strideId); } SpirvInstruction * diff --git a/tools/clang/lib/SPIRV/SpirvInstruction.cpp b/tools/clang/lib/SPIRV/SpirvInstruction.cpp index 24b433c611..7f6cc09bea 100644 --- a/tools/clang/lib/SPIRV/SpirvInstruction.cpp +++ b/tools/clang/lib/SPIRV/SpirvInstruction.cpp @@ -90,6 +90,7 @@ DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvCopyObject) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvSampledImage) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvSelect) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvSpecConstantBinaryOp) +DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvSpecConstantTernaryOp) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvSpecConstantUnaryOp) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvStore) DEFINE_INVOKE_VISITOR_FOR_CLASS(SpirvNullaryOp) @@ -1041,6 +1042,13 @@ SpirvSpecConstantBinaryOp::SpirvSpecConstantBinaryOp(spv::Op specConstantOp, resultType, loc), specOp(specConstantOp), operand1(op1), operand2(op2) {} +SpirvSpecConstantTernaryOp::SpirvSpecConstantTernaryOp( + spv::Op specConstantOp, QualType resultType, SourceLocation loc, + SpirvInstruction *op1, SpirvInstruction *op2, SpirvInstruction *op3) + : SpirvInstruction(IK_SpecConstantTernaryOp, spv::Op::OpSpecConstantOp, + resultType, loc), + specOp(specConstantOp), operand1(op1), operand2(op2), operand3(op3) {} + SpirvSpecConstantUnaryOp::SpirvSpecConstantUnaryOp(spv::Op specConstantOp, QualType resultType, SourceLocation loc, diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl index 29758fbd8e..64d4a94899 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl @@ -1,60 +1,64 @@ // RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s -// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s --check-prefix=DEDUP +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s --check-prefix=COUNT // Verifies the default descriptor-heap array stride. // -// With no manual intervention, every heap runtime array must be decorated with ArrayStrideIdEXT -// referencing an OpConstantSizeOfEXT of the array's element (descriptor) type: +// The HLSL SM6.6 ResourceDescriptorHeap is a single flat array in which the +// client may place any resource descriptor at any slot. To match DX12 +// semantics, all resource descriptor arrays must share one stride equal to +// the largest resource descriptor size: max(sizeof(image), sizeof(buffer)). +// Images and buffers are the two resource descriptor categories defined by +// VkPhysicalDeviceDescriptorHeapPropertiesEXT (imageDescriptorSize / +// bufferDescriptorSize); textures lower to OpTypeImage, so image/buffer +// covers all relevant HLSL resource kinds. +// Both sizes are driver defined and known only at pipeline creation time, so +// the maximum is computed with OpSpecConstantOp over two OpConstantSizeOfEXT +// placeholders. // -// StructuredBuffer / RWStructuredBuffer / ByteAddressBuffer -> OpTypeBufferEXT StorageBuffer -// ConstantBuffer -> OpTypeBufferEXT Uniform -// Texture2D -> OpTypeImage -// SamplerState -> OpTypeSampler +// The sampler heap holds a single descriptor type, so its stride is simply the +// sampler descriptor size. // -// The OpConstantSizeOfEXT result type is %uint +// OpConstantSizeOfEXT result type is %uint. // CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant // Element (descriptor) types. // CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer // CHECK-DAG: %[[UBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform -// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D // CHECK-DAG: %[[SamplerDesc:[a-zA-Z0-9_]+]] = OpTypeSampler -// Heap runtime arrays of those element types. +// Image descriptor type. Texture2D lowers to this same type +// (Dim2D, depth=0, not-arrayed, non-MS, sampled, Unknown format), so +// %[[TexDesc]] serves as both the placeholder for the size calculation and +// the real descriptor type for the Texture2D access below. +// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown + +// Heap runtime arrays of the accessed element types. // CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] // CHECK-DAG: %[[UBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[UBufDesc]] // CHECK-DAG: %[[TexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexDesc]] // CHECK-DAG: %[[SamplerArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerDesc]] -// One OpConstantSizeOfEXT per distinct element type, result type %uint. -// CHECK-DAG: %[[SBBufSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SBBufDesc]] -// CHECK-DAG: %[[UBufSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBufDesc]] -// CHECK-DAG: %[[TexSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[TexDesc]] -// CHECK-DAG: %[[SamplerSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SamplerDesc]] - -// Each heap array is decorated ArrayStrideIdEXT with its element's sizeof. -// CHECK-DAG: OpDecorateId %[[SBBufArray]] ArrayStrideIdEXT %[[SBBufSize]] -// CHECK-DAG: OpDecorateId %[[UBufArray]] ArrayStrideIdEXT %[[UBufSize]] -// CHECK-DAG: OpDecorateId %[[TexArray]] ArrayStrideIdEXT %[[TexSize]] -// CHECK-DAG: OpDecorateId %[[SamplerArray]] ArrayStrideIdEXT %[[SamplerSize]] - -// No literal default stride decoration on any heap array. -// CHECK-NOT: OpDecorate %{{.*}} ArrayStride 64 -// CHECK-NOT: OpDecorate %{{.*}} ArrayStride 32 - -// Dedup: StructuredBuffer and ByteAddressBuffer below both lower to the same -// OpTypeBufferEXT StorageBuffer descriptor, so its OpConstantSizeOfEXT must be -// emitted exactly ONCE and shared, not one per heap access. +// Resource stride = max(image_size, buffer_size); sampler stride = sampler_size. +// CHECK-DAG: %[[ImgSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[TexDesc]] +// CHECK-DAG: %[[BufSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBufDesc]] +// CHECK-DAG: %[[ImgBigger:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSize]] %[[BufSize]] +// CHECK-DAG: %[[ResSize:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[ImgBigger]] %[[ImgSize]] %[[BufSize]] +// CHECK-DAG: %[[SampSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SamplerDesc]] -// Separate prefix: annotations precede the constants section in SPIR-V, so a -// trailing ordered check in the main pass would search past the sizeof. -// {{$}} anchors the match: "StorageBuffer" is a prefix of "StorageBuffer_0" -// (the Uniform descriptor name), so without it DEDUP-NOT would false-fire. +// Every resource runtime array shares the one resource stride; the sampler +// array uses the sampler size. +// CHECK-DAG: OpDecorateId %[[SBBufArray]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[UBufArray]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[TexArray]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[SamplerArray]] ArrayStrideIdEXT %[[SampSize]] -// DEDUP: %[[SBDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer -// DEDUP-COUNT-1: OpConstantSizeOfEXT %uint %[[SBDesc]]{{$}} -// DEDUP-NOT: OpConstantSizeOfEXT %uint %[[SBDesc]]{{$}} +// Three distinct descriptor heap sizes exist: +// the image/Texture2D type (%[[TexDesc]]), the Uniform buffer, and the sampler. +// Regression test: make sure of no per-element nor per acces behavior exists +// otherwise StorageBuffer would add a fourth, pushing the count above three. +// COUNT-COUNT-3: OpConstantSizeOfEXT %uint +// COUNT-NOT: OpConstantSizeOfEXT %uint struct Constants { uint value; @@ -66,10 +70,12 @@ RWStructuredBuffer output : register(u0); void main(uint3 tid : SV_DispatchThreadID) { StructuredBuffer sbuf = ResourceDescriptorHeap[0]; ByteAddressBuffer babuf = ResourceDescriptorHeap[1]; - ConstantBuffer cbuf = ResourceDescriptorHeap[2]; - Texture2D tex = ResourceDescriptorHeap[3]; + Texture2D tex = ResourceDescriptorHeap[2]; + ConstantBuffer cbuf = ResourceDescriptorHeap[3]; + ConstantBuffer cbuf2 = ResourceDescriptorHeap[3]; + SamplerState samp = SamplerDescriptorHeap[0]; float4 color = tex.SampleLevel(samp, float2(0, 0), 0); - output[tid.x] = color + sbuf.Load(tid.x) + babuf.Load(tid.x * 4) + cbuf.value; + output[tid.x] = color + sbuf.Load(tid.x) + babuf.Load(tid.x * 4) + cbuf.value + cbuf2.value; } diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl index 37d45993a7..c44da5b066 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl @@ -19,15 +19,12 @@ // CHECK-DAG: %[[RA_RW2DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW2DArrType]]{{$}} // CHECK-DAG: %[[RA_RW3D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RW3DType]]{{$}} -// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. -// CHECK-DAG: %[[RW1DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW1DType]] -// CHECK-DAG: %[[RW1DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW1DArrType]] -// CHECK-DAG: %[[RW2DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW2DArrType]] -// CHECK-DAG: %[[RW3DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RW3DType]] -// CHECK-DAG: OpDecorateId %[[RA_RW1D]] ArrayStrideIdEXT %[[RW1DSize]] -// CHECK-DAG: OpDecorateId %[[RA_RW1DArr]] ArrayStrideIdEXT %[[RW1DArrSize]] -// CHECK-DAG: OpDecorateId %[[RA_RW2DArr]] ArrayStrideIdEXT %[[RW2DArrSize]] -// CHECK-DAG: OpDecorateId %[[RA_RW3D]] ArrayStrideIdEXT %[[RW3DSize]] +// All RWTextures are resource descriptors, so every array shares one resource stride +// (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) +// CHECK-DAG: OpDecorateId %[[RA_RW1D]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_RW1DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW2DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW3D]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl index 274bcd11bc..c489f08d0c 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl @@ -19,15 +19,12 @@ // CHECK-DAG: %[[RA_Tex2DArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DArrType]]{{$}} // CHECK-DAG: %[[RA_Tex3D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex3DType]]{{$}} -// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. -// CHECK-DAG: %[[Tex1DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex1DType]] -// CHECK-DAG: %[[Tex1DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex1DArrType]] -// CHECK-DAG: %[[Tex2DArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex2DArrType]] -// CHECK-DAG: %[[Tex3DSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Tex3DType]] -// CHECK-DAG: OpDecorateId %[[RA_Tex1D]] ArrayStrideIdEXT %[[Tex1DSize]] -// CHECK-DAG: OpDecorateId %[[RA_Tex1DArr]] ArrayStrideIdEXT %[[Tex1DArrSize]] -// CHECK-DAG: OpDecorateId %[[RA_Tex2DArr]] ArrayStrideIdEXT %[[Tex2DArrSize]] -// CHECK-DAG: OpDecorateId %[[RA_Tex3D]] ArrayStrideIdEXT %[[Tex3DSize]] +// All textures are resource descriptors, so every array shares one resource stride +// (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) +// CHECK-DAG: OpDecorateId %[[RA_Tex1D]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_Tex1DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex2DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex3D]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl index cd7e2db43f..d99312409f 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl @@ -10,11 +10,10 @@ // CHECK-DAG: %[[RA_MS:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[MSType]]{{$}} // CHECK-DAG: %[[RA_MSArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[MSArrType]]{{$}} -// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. -// CHECK-DAG: %[[MSSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[MSType]] -// CHECK-DAG: %[[MSArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[MSArrType]] -// CHECK-DAG: OpDecorateId %[[RA_MS]] ArrayStrideIdEXT %[[MSSize]] -// CHECK-DAG: OpDecorateId %[[RA_MSArr]] ArrayStrideIdEXT %[[MSArrSize]] +// Both multisampled textures are resource descriptors, so they share one resource stride +// (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) +// CHECK-DAG: OpDecorateId %[[RA_MS]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_MSArr]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl index 84c15a2869..e83e13be26 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl @@ -14,11 +14,10 @@ // CHECK-DAG: %[[RA_BufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[BufferType]]{{$}} // CHECK-DAG: %[[RA_RWBufferType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWBufferType]]{{$}} -// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. -// CHECK-DAG: %[[BufferSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[BufferType]] -// CHECK-DAG: %[[RWBufferSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWBufferType]] -// CHECK-DAG: OpDecorateId %[[RA_BufferType]] ArrayStrideIdEXT %[[BufferSize]] -// CHECK-DAG: OpDecorateId %[[RA_RWBufferType]] ArrayStrideIdEXT %[[RWBufferSize]] +// Buffer and RWBuffer are both resource descriptors, so both arrays share one resource stride +// (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) +// CHECK-DAG: OpDecorateId %[[RA_BufferType]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_RWBufferType]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl index 755b4adefb..19c168ff5f 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl @@ -13,13 +13,11 @@ // CHECK-DAG: %[[RA_CubeArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[CubeArrType]]{{$}} // CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} -// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. -// CHECK-DAG: %[[CubeSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[CubeType]] -// CHECK-DAG: %[[CubeArrSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[CubeArrType]] -// CHECK-DAG: %[[SamplerSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SamplerType]] -// CHECK-DAG: OpDecorateId %[[RA_Cube]] ArrayStrideIdEXT %[[CubeSize]] -// CHECK-DAG: OpDecorateId %[[RA_CubeArr]] ArrayStrideIdEXT %[[CubeArrSize]] -// CHECK-DAG: OpDecorateId %[[RA_Sampler]] ArrayStrideIdEXT %[[SamplerSize]] +// Both cube arrays are resource descriptors and share one resource stride; the sampler array uses the separate sampler stride +// (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) +// CHECK-DAG: OpDecorateId %[[RA_Cube]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_CubeArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Sampler]] ArrayStrideIdEXT %[[SampSize:[a-zA-Z0-9_]+]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant // CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl index d001691ced..67bad07c47 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl @@ -23,15 +23,12 @@ // CHECK-DAG: %[[RWTexIType:[a-zA-Z0-9_]+]] = OpTypeImage %int 2D 2 0 0 2 R32i // CHECK-DAG: %[[RA_RWTexI:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexIType]]{{$}} -// Default heap stride: OpConstantSizeOfEXT + ArrayStrideIdEXT per element type. -// CHECK-DAG: %[[TexUintSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[TexUintType]] -// CHECK-DAG: %[[RWTexF2Size:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWTexF2Type]] -// CHECK-DAG: %[[RWTexU2Size:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWTexU2Type]] -// CHECK-DAG: %[[RWTexISize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[RWTexIType]] -// CHECK-DAG: OpDecorateId %[[RA_TexUint]] ArrayStrideIdEXT %[[TexUintSize]] -// CHECK-DAG: OpDecorateId %[[RA_RWTexF2]] ArrayStrideIdEXT %[[RWTexF2Size]] -// CHECK-DAG: OpDecorateId %[[RA_RWTexU2]] ArrayStrideIdEXT %[[RWTexU2Size]] -// CHECK-DAG: OpDecorateId %[[RA_RWTexI]] ArrayStrideIdEXT %[[RWTexISize]] +// All of these are resource descriptors, so every array shares one resource stride +// (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) +// CHECK-DAG: OpDecorateId %[[RA_TexUint]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexF2]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexU2]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexI]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant From 1fe75435812bb24e264b816d3d3c7442a092b375 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 15 Jul 2026 11:33:44 -0700 Subject: [PATCH 05/45] Fixed correct StorageClass for descriptor heap buffer alias pointers TODO --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 50 ++++++++++++-------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index d979918802..4db3d9fb3b 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5270,36 +5270,30 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( spvContext.getUntypedPointerKHRType(spv::StorageClass::UniformConstant); LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, spvBuilder); - const SpirvType *bufferDataType = lowerTypeVisitor.lowerType( - resourceType, SpirvLayoutRule::Void, llvm::None, baseExpr->getExprLoc()); - - const SpirvPointerType *bufferDataPointerType = nullptr; - SpirvLayoutRule layoutRule = spirvOptions.sBufferLayoutRule; - if (isConstantTextureBuffer(resourceType)) { - layoutRule = isConstantBuffer(resourceType) - ? spirvOptions.cBufferLayoutRule - : spirvOptions.tBufferLayoutRule; - bufferDataPointerType = spvContext.getPointerType( - bufferDataType, getDescriptorHeapBufferStorageClass(resourceType)); - } else { - bufferDataPointerType = dyn_cast(bufferDataType); - } - if (!bufferDataPointerType) { - emitError("descriptor heap buffer type lowering failed", - expr->getExprLoc()); - return nullptr; - } + // Select storage class and concrete layout rule for this buffer kind. + // ConstantBuffer -> Uniform (UBO); all others -> StorageBuffer (SSBO). + // Passing the concrete layout rule (not Void) causes lowerType to return + // the bare struct/buffer type rather than a Uniform alias pointer, so we + // can wrap it with the correct storage class here without touching + // LowerTypeVisitor or relying on RemoveBufferBlockVisitor to fix it later. + const spv::StorageClass bufferSC = + getDescriptorHeapBufferStorageClass(resourceType); + SpirvLayoutRule layoutRule; + if (isConstantBuffer(resourceType)) + layoutRule = spirvOptions.cBufferLayoutRule; + else if (isTextureBuffer(resourceType)) + layoutRule = spirvOptions.tBufferLayoutRule; + else + layoutRule = spirvOptions.sBufferLayoutRule; + + const SpirvType *bufferDataType = lowerTypeVisitor.lowerType( + resourceType, layoutRule, llvm::None, baseExpr->getExprLoc()); + const SpirvPointerType *bufferDataPointerType = + spvContext.getPointerType(bufferDataType, bufferSC); - // ConstantBuffer -> Uniform (UBO); all others -> StorageBuffer (SSBO) - // TODO: Remove this manual override once LowerTypeVisitor returns the - // correct StorageClass for descriptor-heap alias pointer types - // (currently it returns Uniform for all of them). - const spv::StorageClass bufferExtSC = isConstantBuffer(resourceType) - ? spv::StorageClass::Uniform - : spv::StorageClass::StorageBuffer; const BufferEXTType *bufferDescriptorType = - spvContext.getBufferEXTType(bufferExtSC); + spvContext.getBufferEXTType(bufferSC); const SpirvType *arrayType = getDescriptorHeapRuntimeArrayType(bufferDescriptorType); SpirvUntypedAccessChainKHR *untypedAccessChainPtr = @@ -5309,7 +5303,7 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( SpirvUnaryOp *bufferDataPtr = spvBuilder.createUnaryOp( spv::Op::OpBufferPointerEXT, bufferDataPointerType, untypedAccessChainPtr, baseExpr->getExprLoc()); - bufferDataPtr->setStorageClass(bufferDataPointerType->getStorageClass()); + bufferDataPtr->setStorageClass(bufferSC); bufferDataPtr->setLayoutRule(layoutRule); bufferDataPtr->setRValue(false); if (isRasterizerOrderedView(resourceType)) { From 5f82d2c55d19a63a8dfecafa71792452a343b643 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 22 Jul 2026 15:50:05 -0700 Subject: [PATCH 06/45] Ammend keyword usage to follow LLVM Coding Standards --- tools/clang/lib/SPIRV/EmitVisitor.cpp | 4 ++-- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tools/clang/lib/SPIRV/EmitVisitor.cpp b/tools/clang/lib/SPIRV/EmitVisitor.cpp index 6cf8450f34..aebe48180a 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.cpp +++ b/tools/clang/lib/SPIRV/EmitVisitor.cpp @@ -2732,13 +2732,13 @@ uint32_t EmitTypeHandler::emitType(const SpirvType *type) { curTypeInst.push_back(elemTypeId); finalizeTypeInstruction(); - if (auto *strideId = raType->getArrayStrideId()) { + if (SpirvInstruction *strideId = raType->getArrayStrideId()) { // SPV_EXT_descriptor_heap: stride given by a constant rather than a // literal, emitted as OpDecorateId ... ArrayStrideIdEXT . emitDecoration(id, spv::Decoration::ArrayStrideIdEXT, {getOrAssignResultId(strideId)}, llvm::None, /*usesIdParams=*/true); - } else if (auto stride = raType->getStride()) { + } else if (llvm::Optional stride = raType->getStride()) { emitDecoration(id, spv::Decoration::ArrayStride, {stride.getValue()}); } } diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 4db3d9fb3b..abc7d69344 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -3181,7 +3181,7 @@ SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { tryToAssignToDescriptorHeapBuffer(expr)) return aliasResult.getValue(); - auto *rhs = loadIfGLValue(expr->getRHS()); + SpirvInstruction *rhs = loadIfGLValue(expr->getRHS()); tryToAssignDescriptorHeapImageAlias(expr->getLHS(), expr->getRHS()); return processAssignment(expr->getLHS(), rhs, @@ -5088,7 +5088,7 @@ SpirvEmitter::processStructuredBufferLoad(const CXXMemberCallExpr *expr) { auto *zero = spvBuilder.getConstantInt(astContext.IntTy, llvm::APInt(32, 0)); auto *index = doExpr(expr->getArg(0)); - auto *result = + SpirvInstruction *result = derefOrCreatePointerToValue(buffer->getType(), info, structType, {zero, index}, buffer->getExprLoc(), range); @@ -5114,7 +5114,7 @@ void SpirvEmitter::markDescriptorHeapCounterUnsupported( } bool SpirvEmitter::isDescriptorHeapCounterUnsupported(const Expr *expr) const { - if (const auto *decl = getReferencedDef(expr)) + if (const DeclaratorDecl *decl = getReferencedDef(expr)) return descriptorHeapUnsupportedCounters.count(decl) != 0; return false; } @@ -5158,7 +5158,7 @@ bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( (!isRWTexture(dstVar->getType()) && !isRWBuffer(dstVar->getType()))) return false; - const auto *src = srcExpr->IgnoreParenCasts(); + const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapImageAccesses.find(src); if (found == descriptorHeapImageAccesses.end()) return false; @@ -5190,7 +5190,7 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( isAKindOfStructuredOrByteBuffer(dstVar->getType()))) return false; - const auto *src = srcExpr->IgnoreParenCasts(); + const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapBufferAccesses.find(src); if (found == descriptorHeapBufferAccesses.end()) return false; @@ -5515,7 +5515,7 @@ SpirvEmitter::getFinalACSBufferCounterInstruction(const Expr *expr) { const CounterIdAliasPair * SpirvEmitter::getFinalACSBufferCounter(const Expr *expr) { // AssocCounter#1: referencing some stand-alone variable - if (const auto *decl = getReferencedDef(expr)) + if (const DeclaratorDecl *decl = getReferencedDef(expr)) return declIdMapper.getOrCreateCounterIdAliasPair(decl); const Expr *expr_withoutcasts = expr->IgnoreParenCasts(); @@ -6005,13 +6005,15 @@ SpirvEmitter::processIntrinsicMemberCall(const CXXMemberCallExpr *expr, retVal = processTextureLevelOfDetail(expr, /* unclamped */ true); break; case IntrinsicOp::MOP_IncrementCounter: - if (auto *counter = incDecRWACSBufferCounter(expr, /*isInc*/ true)) + if (SpirvInstruction *counter = + incDecRWACSBufferCounter(expr, /*isInc*/ true)) retVal = spvBuilder.createUnaryOp( spv::Op::OpBitcast, astContext.UnsignedIntTy, counter, expr->getCallee()->getExprLoc(), expr->getCallee()->getSourceRange()); break; case IntrinsicOp::MOP_DecrementCounter: - if (auto *counter = incDecRWACSBufferCounter(expr, /*isInc*/ false)) + if (SpirvInstruction *counter = + incDecRWACSBufferCounter(expr, /*isInc*/ false)) retVal = spvBuilder.createUnaryOp( spv::Op::OpBitcast, astContext.UnsignedIntTy, counter, expr->getCallee()->getExprLoc(), expr->getCallee()->getSourceRange()); @@ -10958,7 +10960,7 @@ SpirvEmitter::processIntrinsicInterlockedMethod(const CallExpr *expr, } if (!ptr) { - auto *baseInstr = doExpr(base); + SpirvInstruction *baseInstr = doExpr(base); if (baseInstr->isRValue()) { // OpImageTexelPointer's Image argument must have a type of // OpTypePointer with Type OpTypeImage. Need to create a temporary From 9bd2e2ddfac1df6f6a543d8fcb8382950d2655fb Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 30 Jul 2026 14:30:00 -0700 Subject: [PATCH 07/45] fix stride test --- ...sm6_6.descriptorheap.ext.array-stride.hlsl | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl index 64d4a94899..e690564fb8 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl @@ -11,6 +11,11 @@ // VkPhysicalDeviceDescriptorHeapPropertiesEXT (imageDescriptorSize / // bufferDescriptorSize); textures lower to OpTypeImage, so image/buffer // covers all relevant HLSL resource kinds. +// +// NOTE: This covers the case with no RaytracingAccelerationStructures. +// When present, the stride formula expands to a three-way max: +// max(max(sizeof(image), sizeof(buffer)), sizeof(accelerationStructure)). +// // Both sizes are driver defined and known only at pipeline creation time, so // the maximum is computed with OpSpecConstantOp over two OpConstantSizeOfEXT // placeholders. @@ -27,11 +32,16 @@ // CHECK-DAG: %[[UBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform // CHECK-DAG: %[[SamplerDesc:[a-zA-Z0-9_]+]] = OpTypeSampler -// Image descriptor type. Texture2D lowers to this same type -// (Dim2D, depth=0, not-arrayed, non-MS, sampled, Unknown format), so -// %[[TexDesc]] serves as both the placeholder for the size calculation and -// the real descriptor type for the Texture2D access below. -// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown +// Two distinct OpTypeImage types appear in the output: +// +// 1) ImgPlaceholder: a canonical sampled 2D float image used only as +// the operand to OpConstantSizeOfEXT. +// +// 2) TexDesc: the actual lowered type for Texture2D. +// This is the element type of the texture heap runtime array. +// +// CHECK-DAG: %[[ImgPlaceholder:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown +// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown // Heap runtime arrays of the accessed element types. // CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] @@ -40,22 +50,23 @@ // CHECK-DAG: %[[SamplerArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerDesc]] // Resource stride = max(image_size, buffer_size); sampler stride = sampler_size. -// CHECK-DAG: %[[ImgSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[TexDesc]] +// The size query uses ImgPlaceholder (depth=0); the driver returns the same +// imageDescriptorSize regardless of which image subtype is used as the operand. +// CHECK-DAG: %[[ImgSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[ImgPlaceholder]] // CHECK-DAG: %[[BufSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBufDesc]] // CHECK-DAG: %[[ImgBigger:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSize]] %[[BufSize]] // CHECK-DAG: %[[ResSize:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[ImgBigger]] %[[ImgSize]] %[[BufSize]] // CHECK-DAG: %[[SampSize:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[SamplerDesc]] -// Every resource runtime array shares the one resource stride; the sampler -// array uses the sampler size. +// Every resource runtime array shares the one resource stride. +// The sampler array just uses the sampler size as it's stride. // CHECK-DAG: OpDecorateId %[[SBBufArray]] ArrayStrideIdEXT %[[ResSize]] // CHECK-DAG: OpDecorateId %[[UBufArray]] ArrayStrideIdEXT %[[ResSize]] // CHECK-DAG: OpDecorateId %[[TexArray]] ArrayStrideIdEXT %[[ResSize]] // CHECK-DAG: OpDecorateId %[[SamplerArray]] ArrayStrideIdEXT %[[SampSize]] -// Three distinct descriptor heap sizes exist: -// the image/Texture2D type (%[[TexDesc]]), the Uniform buffer, and the sampler. -// Regression test: make sure of no per-element nor per acces behavior exists +// Three distinct OpConstantSizeOfEXT calls exist: ImgPlaceholder, Uniform +// buffer, and sampler. Regression test: no per-element or per-access behavior; // otherwise StorageBuffer would add a fourth, pushing the count above three. // COUNT-COUNT-3: OpConstantSizeOfEXT %uint // COUNT-NOT: OpConstantSizeOfEXT %uint From a5f3f45244be75c7ac12309be9af3e8dd5691c29 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 31 Jul 2026 14:31:10 -0700 Subject: [PATCH 08/45] Cover arrayStrideId uniquing in SpirvContextTest --- .../unittests/SPIRV/SpirvContextTest.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tools/clang/unittests/SPIRV/SpirvContextTest.cpp b/tools/clang/unittests/SPIRV/SpirvContextTest.cpp index 08d33e4892..5cc9bb6b74 100644 --- a/tools/clang/unittests/SPIRV/SpirvContextTest.cpp +++ b/tools/clang/unittests/SPIRV/SpirvContextTest.cpp @@ -329,6 +329,34 @@ TEST_F(SpirvContextTest, RuntimeArrayTypeUnique3) { spvContext.getRuntimeArrayType(int32, 32)); } +TEST_F(SpirvContextTest, RuntimeArrayTypeUnique4) { + // Uniquing behaviour for the arrayStrideId (SpirvInstruction*) overload. + SpirvContext &spvContext = getSpirvContext(); + const auto *int32 = spvContext.getSIntType(32); + + // Two distinct pointer values stand in for two distinct SpirvInstruction + // operands. getRuntimeArrayType compares them by pointer identity and never + // dereferences them during the lookup. + auto *instr1 = + reinterpret_cast(static_cast(1)); + auto *instr2 = + reinterpret_cast(static_cast(2)); + + // 1. Different arrayStrideId values -> different RuntimeArrayType pointers. + EXPECT_NE(spvContext.getRuntimeArrayType(int32, llvm::None, instr1), + spvContext.getRuntimeArrayType(int32, llvm::None, instr2)); + + // 2. Same arrayStrideId value -> same RuntimeArrayType pointer (uniqued). + EXPECT_EQ(spvContext.getRuntimeArrayType(int32, llvm::None, instr1), + spvContext.getRuntimeArrayType(int32, llvm::None, instr1)); + + // 3. Literal-stride type != id-stride type even when the numeric values + // could be considered equivalent (arrayStride=64, no id) vs + // (arrayStride=None, instr1 as id). + EXPECT_NE(spvContext.getRuntimeArrayType(int32, 64), + spvContext.getRuntimeArrayType(int32, llvm::None, instr1)); +} + TEST_F(SpirvContextTest, PointerTypeUnique1) { SpirvContext &spvContext = getSpirvContext(); const auto *int32 = spvContext.getSIntType(32); From 274b51e22a9e6bc6cd8e215c673b9d1eb2b4ef20 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 31 Jul 2026 15:27:24 -0700 Subject: [PATCH 09/45] Diagnose mixed bound/heap aliasing and correct documentation --- docs/SPIR-V.rst | 58 +++++++-- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 116 ++++++++++++++++-- tools/clang/lib/SPIRV/SpirvEmitter.h | 47 ++++++- ....descriptorheap.ext.mixed-alias.error.hlsl | 92 ++++++++++++++ 4 files changed, 289 insertions(+), 24 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl diff --git a/docs/SPIR-V.rst b/docs/SPIR-V.rst index 042dbfd416..9d04b7e6f8 100644 --- a/docs/SPIR-V.rst +++ b/docs/SPIR-V.rst @@ -2101,14 +2101,16 @@ objects as untyped variables in ``UniformConstant`` storage class: The concrete descriptor type is selected at each heap access. For image, sampler, and texel buffer resources, DXC forms a runtime array of that -descriptor type and decorates the array with a byte ``ArrayStride``, then uses -``OpUntypedAccessChainKHR`` followed by ``OpLoad``: +descriptor type, decorates the array with a byte stride, and uses +``OpUntypedAccessChainKHR`` followed by ``OpLoad``. The stride is an +``ArrayStrideIdEXT`` decoration referencing a specialization constant rather +than a literal ``ArrayStride`` (see `Descriptor heap array stride`_ below): .. code:: spirv %image_type = OpTypeImage %float 2D 2 0 0 1 Unknown %image_array = OpTypeRuntimeArray %image_type - OpDecorate %image_array ArrayStride 64 + OpDecorateId %image_array ArrayStrideIdEXT %resource_stride %descriptor = OpUntypedAccessChainKHR %uptr_uc %image_array %resource_heap %index %image = OpLoad %image_type %descriptor @@ -2122,7 +2124,7 @@ example, ``ConstantBuffer`` uses ``Uniform`` and ``TextureBuffer`` uses %buffer_type = OpTypeBufferEXT Uniform %buffer_array = OpTypeRuntimeArray %buffer_type - OpDecorate %buffer_array ArrayStride 64 + OpDecorateId %buffer_array ArrayStrideIdEXT %resource_stride %descriptor = OpUntypedAccessChainKHR %uptr_uc %buffer_array %resource_heap %index %buffer_ptr = OpBufferPointerEXT %_ptr_Uniform_type_BufferData %descriptor @@ -2146,9 +2148,11 @@ StructuredBuffer/RWStructuredBuffer without associated counter operations, ByteAddressBuffer/RWByteAddressBuffer, ConstantBuffer, and TextureBuffer heap loads, including direct field and array-element accesses for ``ConstantBuffer`` and ``TextureBuffer``. ``NonUniformResourceIndex`` is -accepted but the ``NonUniform`` decoration is not emitted on -``OpUntypedAccessChainKHR`` or the loaded value; ``SPV_EXT_descriptor_heap`` -deprecates the ``NonUniform`` decoration for heap accesses. +accepted, but no ``NonUniform`` decoration is emitted on the +``OpUntypedAccessChainKHR`` result or on the loaded descriptor; +``SPV_EXT_descriptor_heap`` deprecates the decoration for heap accesses and +drivers handle divergent heap indices natively. The index operand itself may +still carry ``NonUniform`` from the surrounding expression. Append/consume structured buffers and UAV counter heap lowering are not supported by the native descriptor heap path yet. Those forms should continue @@ -2159,6 +2163,46 @@ associated counter operations such as ``IncrementCounter`` and ``DecrementCounter`` emit a diagnostic because the native descriptor heap path does not recover an associated counter descriptor. +A local resource variable initialized from a heap access is resolved entirely at +compile time: the variable is recorded as an alias for the heap index, and every +later use is re-lowered as a fresh access chain rather than as a load of a stored +descriptor handle. This is sound only when the variable holds a heap descriptor +on every path that reaches the use. DXC therefore rejects a variable that holds +both a bound resource and a heap descriptor, whether through a conditional +assignment, a reassignment back to a bound resource, or an assignment inside a +loop:: + + error: mixing bound and descriptor heap resources in the same variable is not + supported with SPV_EXT_descriptor_heap + +Supporting these forms requires modelling the alias as a value with real +control-flow merges instead of as compile-time state. + +Two further restrictions on the heap access expression itself produce +diagnostics. The object being subscripted must be a direct reference to the +builtin ``ResourceDescriptorHeap`` or ``SamplerDescriptorHeap`` variable, and +the subscript result must be immediately converted to a concrete resource type +so that DXC can select a descriptor type for the access. A subscript whose +result is discarded, or used in a context that supplies no target resource +type, is rejected. + +Descriptor heap array stride +++++++++++++++++++++++++++++ + +All resource heap runtime arrays share a single ``ArrayStrideIdEXT`` decoration +rather than a literal ``ArrayStride``, because descriptor sizes are not known +until pipeline creation. The shared value is built from ``OpConstantSizeOfEXT`` +and ``OpSpecConstantOp`` and evaluates to +``max(sizeof(image_descriptor), sizeof(buffer_descriptor))``. The sampler heap +carries its own ``ArrayStrideIdEXT`` equal to ``sizeof(sampler_descriptor)``. + +The ``OpConstantSizeOfEXT`` operands are placeholder types chosen only for their +descriptor class. All image types report the same descriptor size, so the +placeholder is a plain sampled 2D float image and bears no relation to the image +types the shader actually uses; a module will normally contain both the +placeholder type and the distinct image types its heap accesses lower to. The +stride value is built once and cached on first use. + HLSL Expressions ================ diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index abc7d69344..bd39cbb6af 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2250,6 +2250,7 @@ void SpirvEmitter::doVarDecl(const VarDecl *decl) { spvBuilder.createStore(var, constInit, loc, range); } else { storeValue(var, loadIfGLValue(init), decl->getType(), loc, range); + diagnoseDescriptorHeapAliasMixing(decl, init, loc); tryToAssignDescriptorHeapImageAlias(decl, init); } @@ -3168,6 +3169,27 @@ SpirvEmitter::tryToAssignToDescriptorHeapBuffer( return emitDescriptorHeapBufferPointer(decl, assignExpr->getExprLoc()); } +llvm::Optional +SpirvEmitter::tryToAssignToDescriptorHeapAlias( + const BinaryOperator *assignExpr) { + if (!spirvOptions.useDescriptorHeap) + return llvm::None; + + const auto *dstVar = + dyn_cast_or_null(getReferencedDef(assignExpr->getLHS())); + if (!diagnoseDescriptorHeapAliasMixing(dstVar, assignExpr->getRHS(), + assignExpr->getExprLoc())) + return tryToAssignToDescriptorHeapBuffer(assignExpr); + + // The assignment was rejected. A heap buffer alias is represented only by its + // index variable, so the normal path has no destination to store into and + // would null-deref; consume the assignment instead. Image aliases do have a + // function variable, so they can fall through to the plain handle store. + if (descriptorHeapBufferAliasVars.count(dstVar)) + return static_cast(nullptr); + return llvm::None; +} + SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { const auto opcode = expr->getOpcode(); @@ -3178,7 +3200,7 @@ SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { tryToAssignCounterVar(expr->getLHS(), expr->getRHS()); if (llvm::Optional aliasResult = - tryToAssignToDescriptorHeapBuffer(expr)) + tryToAssignToDescriptorHeapAlias(expr)) return aliasResult.getValue(); SpirvInstruction *rhs = loadIfGLValue(expr->getRHS()); @@ -5109,13 +5131,19 @@ SpirvEmitter::processStructuredBufferLoad(const CXXMemberCallExpr *expr) { void SpirvEmitter::markDescriptorHeapCounterUnsupported( const DeclaratorDecl *decl) { - if (decl) - descriptorHeapUnsupportedCounters.insert(decl); + if (const auto *var = dyn_cast_or_null(decl)) { + auto it = descriptorHeapBufferAliasVars.find(var); + if (it != descriptorHeapBufferAliasVars.end()) + it->second.counterUnsupported = true; + } } bool SpirvEmitter::isDescriptorHeapCounterUnsupported(const Expr *expr) const { - if (const DeclaratorDecl *decl = getReferencedDef(expr)) - return descriptorHeapUnsupportedCounters.count(decl) != 0; + if (const auto *var = dyn_cast_or_null(getReferencedDef(expr))) { + auto it = descriptorHeapBufferAliasVars.find(var); + if (it != descriptorHeapBufferAliasVars.end()) + return it->second.counterUnsupported; + } return false; } @@ -5148,6 +5176,52 @@ SpirvEmitter::createDescriptorHeapIndexVar(const VarDecl *dstVar) { name); } +bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, + const Expr *srcExpr, + SourceLocation loc) { + if (!spirvOptions.useDescriptorHeap || !dstVar || !srcExpr) + return false; + + // Report once per variable; every later assignment stays rejected. + auto stateIt = descriptorHeapVarState.find(dstVar); + if (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Mixed) + return true; + + // Only the resource kinds that use the compile-time alias mechanism can be + // miscompiled by a mixed assignment. Other resources are stored into a real + // function variable, which merges correctly across control flow. + const QualType dstType = dstVar->getType(); + if (!isRWTexture(dstType) && !isRWBuffer(dstType) && + !isConstantTextureBuffer(dstType) && + !isAKindOfStructuredOrByteBuffer(dstType)) + return false; + + const bool srcIsHeap = isDescriptorHeap(srcExpr->IgnoreParenCasts()); + const bool wasHeap = descriptorHeapImageAliasVars.count(dstVar) || + descriptorHeapBufferAliasVars.count(dstVar); + const bool wasBound = stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Bound; + const bool mixingDetected = + (srcIsHeap && wasBound) || (!srcIsHeap && wasHeap); + + if (mixingDetected) { + emitError("mixing bound and descriptor heap resources in the same variable " + "is not supported with SPV_EXT_descriptor_heap", + loc); + // Leave any recorded alias in place. For heap-initialized buffer variables, + // doVarDecl returns early before calling createFnVar, so the alias is the + // only handle they have. Erasing it would crash downstream uses; keeping it + // is safe because the emitted error already fails the compilation. + descriptorHeapVarState[dstVar] = DescriptorHeapVarState::Mixed; + return true; + } + + if (!srcIsHeap) + descriptorHeapVarState[dstVar] = DescriptorHeapVarState::Bound; + return false; +} + bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( const DeclaratorDecl *dstDecl, const Expr *srcExpr) { if (!spirvOptions.useDescriptorHeap || !dstDecl || !srcExpr) @@ -5158,6 +5232,13 @@ bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( (!isRWTexture(dstVar->getType()) && !isRWBuffer(dstVar->getType()))) return false; + { + auto stateIt = descriptorHeapVarState.find(dstVar); + if (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Mixed) + return false; + } + const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapImageAccesses.find(src); if (found == descriptorHeapImageAccesses.end()) @@ -5190,14 +5271,18 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( isAKindOfStructuredOrByteBuffer(dstVar->getType()))) return false; + { + auto stateIt = descriptorHeapVarState.find(dstVar); + if (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Mixed) + return false; + } + const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapBufferAccesses.find(src); if (found == descriptorHeapBufferAccesses.end()) return false; - if (isRWStructuredBuffer(dstVar->getType())) - markDescriptorHeapCounterUnsupported(dstVar); - auto &alias = descriptorHeapBufferAliasVars[dstVar]; if (!alias.indexVar) alias.indexVar = createDescriptorHeapIndexVar(dstVar); @@ -5205,6 +5290,7 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( alias.arrayType = found->second.arrayType; alias.heap = found->second.heap; alias.layoutRule = found->second.layoutRule; + alias.counterUnsupported = isRWStructuredBuffer(dstVar->getType()); storeDescriptorHeapIndex(alias.indexVar, found->second.index, found->second.indexType, srcExpr); return true; @@ -5346,13 +5432,19 @@ SpirvEmitter::incDecRWACSBufferCounter(const CXXMemberCallExpr *expr, return nullptr; } + // Only heap-loaded append/consume buffers are unsupported. Explicitly bound + // ones keep working in native heap mode, so key the diagnostic on the object + // actually having a heap alias rather than on the option alone. if (spirvOptions.useDescriptorHeap && (isAppendStructuredBuffer(object->getType()) || isConsumeStructuredBuffer(object->getType()))) { - emitError("append/consume structured buffers are not supported with " - "SPV_EXT_descriptor_heap", - expr->getCallee()->getExprLoc()); - return nullptr; + const auto *objVar = dyn_cast_or_null(getReferencedDef(object)); + if (objVar && descriptorHeapBufferAliasVars.count(objVar)) { + emitError("append/consume structured buffers are not supported with " + "SPV_EXT_descriptor_heap", + expr->getCallee()->getExprLoc()); + return nullptr; + } } auto *counter = getFinalACSBufferCounterInstruction(object); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 9f206fcb17..ddc3caa18f 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -1224,7 +1224,8 @@ class SpirvEmitter : public ASTConsumer { /// \brief Returns true if counter operations on the resource expression are /// known to be unsupported because the resource came from - /// ResourceDescriptorHeap. + /// ResourceDescriptorHeap. Consults the counterUnsupported field of the + /// buffer alias entry for the referenced variable. bool isDescriptorHeapCounterUnsupported(const Expr *expr) const; /// \brief Records the descriptor heap index assigned to a local image @@ -1241,6 +1242,24 @@ class SpirvEmitter : public ASTConsumer { bool tryToAssignDescriptorHeapBufferAlias(const Expr *dstExpr, const Expr *srcExpr); + /// \brief Diagnoses a local resource variable assigned from both a bound + /// resource and ResourceDescriptorHeap. + /// + /// The alias table maps each aliased VarDecl to heap descriptor info at + /// compile time. Once a variable is recorded as an alias, every later use is + /// re-lowered as a heap access chain, ignoring control flow. That is only + /// correct when the variable holds a heap descriptor on every path reaching + /// the use, so assigning it from both kinds of source must be diagnosed + /// rather than silently miscompiled. + /// + /// Returns true if the assignment was rejected (either a new diagnostic was + /// emitted, or the variable was already diagnosed). Callers need not act on + /// the return value: the alias-recording helpers check descriptorHeapVarState + /// themselves and skip rejected variables automatically. + bool diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, + const Expr *srcExpr, + SourceLocation loc); + /// \brief Creates the ".descriptor.index" function variable used to /// remember the descriptor heap index of a local resource alias dstVar. SpirvVariable *createDescriptorHeapIndexVar(const VarDecl *dstVar); @@ -1258,6 +1277,14 @@ class SpirvEmitter : public ASTConsumer { llvm::Optional tryToAssignToDescriptorHeapBuffer(const BinaryOperator *assignExpr); + /// \brief Entry point for the descriptor-heap handling of a simple + /// assignment: rejects a destination that mixes bound and heap resources, + /// then defers to tryToAssignToDescriptorHeapBuffer. Returns None if the + /// assignment needs no heap-specific treatment and the caller should lower it + /// as a normal assignment. + llvm::Optional + tryToAssignToDescriptorHeapAlias(const BinaryOperator *assignExpr); + /// \brief Emits the instructions that re-derive the buffer-data pointer for a /// descriptor-heap buffer alias decl (OpLoad of the saved index, then /// OpUntypedAccessChainKHR + OpBufferPointerEXT). Returns nullptr if decl @@ -1640,6 +1667,10 @@ class SpirvEmitter : public ASTConsumer { /// The SPIR-V function parameter for the current this object. SpirvInstruction *curThis; + // TODO: The following ~15 descriptorHeap* members and their methods are good + // candidates for refactoring into a dedicated DescriptorHeapAliasEmitter + // helper class. + /// Native descriptor heap image descriptors used to directly form image /// atomics. The emitter is single-use per translation unit, so these /// AST-pointer maps live for the emitter lifetime. @@ -1671,7 +1702,17 @@ class SpirvEmitter : public ASTConsumer { const SpirvType *arrayType; SpirvInstruction *heap; SpirvLayoutRule layoutRule; + bool counterUnsupported = false; }; + /// Tracks per-variable assignment history for the mixed-alias diagnostic. + /// Bound: the variable has been assigned from a bound resource; a later heap + /// assignment to it would be a diagnosable mix. + /// Mixed: mixing was already diagnosed; alias recording stays suppressed for + /// the remainder of the function so the error fires only once. + enum class DescriptorHeapVarState : uint8_t { Bound, Mixed }; + llvm::DenseMap + descriptorHeapVarState; + llvm::DenseMap descriptorHeapImageAccesses; llvm::DenseMap @@ -1681,10 +1722,6 @@ class SpirvEmitter : public ASTConsumer { llvm::DenseMap descriptorHeapBufferAliasVars; - /// RWStructuredBuffer aliases loaded from ResourceDescriptorHeap have no - /// associated UAV counter descriptor in the native descriptor heap path. - llvm::DenseSet descriptorHeapUnsupportedCounters; - /// The source location of a push constant block we have previously seen. /// Invalid means no push constant blocks defined thus far. SourceLocation seenPushConstantAt; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl new file mode 100644 index 0000000000..ce0a3f0878 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl @@ -0,0 +1,92 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=0 %s | FileCheck %s --check-prefix=OK +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=1 %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=2 %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=3 %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=4 %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=5 %s 2>&1 | FileCheck %s + +// Verifies that a local resource variable assigned from both a bound resource +// and ResourceDescriptorHeap is rejected. +// +// The alias table maps each aliased VarDecl to heap descriptor info at compile +// time. Once a variable is recorded as an alias, every later use is re-lowered +// as a heap access chain, ignoring control flow. That is only correct when the +// variable holds a heap descriptor on every path reaching the use, so assigning +// it from both kinds of source must be diagnosed rather than silently +// miscompiled. +// +// Before this was diagnosed, CASE=1 emitted +// %index = OpSelect %uint %cond %uint_1 %undef +// OpUntypedAccessChainKHR ... %resource_heap %index +// i.e. the else path indexed the resource heap with an undefined value, and +// CASE=2 atomically updated heap descriptor 2 after the variable had been +// reassigned to `boundTex`. +// +// Both assignment orderings are detected: bound-then-heap assignments (CASE=1, +// CASE=3, CASE=5) are tracked in descriptorHeapVarState; heap-then-bound +// assignments (CASE=2, CASE=4) are detected by observing that the alias map +// already contains an entry for the variable. + +// CHECK: error: {{.*}}mixing bound and descriptor heap resources in the same variable is not supported with SPV_EXT_descriptor_heap +// OK: OpUntypedImageTexelPointerEXT + +RWByteAddressBuffer outputBytes : register(u0); +RWTexture2D boundTex : register(u1); +RWByteAddressBuffer boundBuf : register(u2); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + uint original; + +#if CASE == 0 + // Control: heap-only reassignment is legal. Both sources are heap descriptors, + // so the alias index is simply updated on each assignment and every path holds + // a heap descriptor. + RWTexture2D heapOnly = ResourceDescriptorHeap[1]; + if (tid.x == 0) + heapOnly = ResourceDescriptorHeap[2]; + InterlockedAdd(heapOnly[tid.xy], 4, original); + +#elif CASE == 1 + // Bound-then-heap (conditional): the heap descriptor is assigned on one path + // only. The else path still holds the bound resource, but the atomic would be + // lowered as a heap access with an undefined index. + RWTexture2D mixed = boundTex; + if (tid.x == 0) + mixed = ResourceDescriptorHeap[1]; + InterlockedAdd(mixed[tid.xy], 1, original); + +#elif CASE == 2 + // Heap-then-bound: the alias would never be cleared, so the atomic would keep + // targeting heap descriptor 2 even after the variable was reassigned to the + // bound texture. + RWTexture2D mixed = ResourceDescriptorHeap[2]; + mixed = boundTex; + InterlockedAdd(mixed[tid.xy], 2, original); + +#elif CASE == 3 + // Bound-then-heap (loop): same as CASE=1 but the heap index is undefined when + // the loop body never executes. + RWTexture2D mixed = boundTex; + for (uint i = 0; i < tid.y; ++i) + mixed = ResourceDescriptorHeap[i]; + InterlockedAdd(mixed[tid.xy], 3, original); + +#elif CASE == 4 + // Buffer heap-then-bound: the alias records heap index 3; the reassignment to + // a bound buffer must be rejected before any load sees the stale alias. + RWByteAddressBuffer mixedBuf = ResourceDescriptorHeap[3]; + mixedBuf = boundBuf; + original = mixedBuf.Load(0); + +#elif CASE == 5 + // Buffer bound-then-heap: the bound assignment is recorded in + // descriptorHeapVarState; the later heap assignment must be rejected. + RWByteAddressBuffer mixedBuf = boundBuf; + mixedBuf = ResourceDescriptorHeap[3]; + original = mixedBuf.Load(0); + +#endif + + outputBytes.Store(0, original); +} From 0066403fa378f000df9ef99a38a8c89114253ef9 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 13 Aug 2026 15:59:36 -0700 Subject: [PATCH 10/45] Diagnose heap buffer alias use in function calls + returns and support alias-to-alias --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 147 +++++++++++++++--- tools/clang/lib/SPIRV/SpirvEmitter.h | 11 +- ...descriptorheap.ext.alias-fn-readwrite.hlsl | 56 +++++++ ...sm6_6.descriptorheap.ext.alias-return.hlsl | 33 ++++ ...6_6.descriptorheap.ext.alias-to-alias.hlsl | 60 +++++++ ...rheap.ext.buffer-alias-fn-param.error.hlsl | 31 ++++ ...scriptorheap.ext.image-alias-fn-param.hlsl | 41 +++++ ....descriptorheap.ext.mixed-alias.error.hlsl | 13 ++ 8 files changed, 371 insertions(+), 21 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-return.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-to-alias.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-alias-fn-param.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index bd39cbb6af..aa6927e73c 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2021,12 +2021,16 @@ void SpirvEmitter::doEnumDecl(const EnumDecl *decl) { bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, const Expr *init) { if (!spirvOptions.useDescriptorHeap || !init || - !isDescriptorHeap(init->IgnoreParenCasts())) + !isHeapSourcedValue(init->IgnoreParenCasts())) return false; if (isConstantTextureBuffer(decl->getType()) || isAKindOfStructuredOrByteBuffer(decl->getType())) { - (void)doExpr(init->IgnoreParenCasts()); + // doExpr populates descriptorHeapBufferAccesses for the direct-subscript + // path; the VarDecl fallback in tryToAssignDescriptorHeapBufferAlias + // bypasses that map, so skip the call to avoid emitting dead instructions. + if (isDescriptorHeap(init->IgnoreParenCasts())) + (void)doExpr(init->IgnoreParenCasts()); tryToAssignDescriptorHeapBufferAlias(decl, init); return true; } @@ -2976,14 +2980,33 @@ void SpirvEmitter::doReturnStmt(const ReturnStmt *stmt) { if (!returnsVoid) { assert(retVal); const Expr *srcExpr = retVal->IgnoreParenCasts(); - if (isDescriptorHeap(srcExpr)) { - const Expr *base = nullptr; - getDescriptorHeapOperands(srcExpr, &base, /* index= */ nullptr); - const Expr *parentExpr = cast(parentMap->getParent(srcExpr)); - QualType resourceType = parentExpr->getType(); - const auto *declRefExpr = dyn_cast(base->IgnoreCasts()); - auto *decl = cast(declRefExpr->getDecl()); - declIdMapper.createResourceHeap(decl, resourceType); + if (isHeapSourcedValue(srcExpr)) { + if (isDescriptorHeap(srcExpr)) { + // Direct heap subscript: register the heap variable so the declaration + // mapper can resolve it for the return instruction. + const Expr *base = nullptr; + getDescriptorHeapOperands(srcExpr, &base, /* index= */ nullptr); + const Expr *parentExpr = cast(parentMap->getParent(srcExpr)); + QualType resourceType = parentExpr->getType(); + const auto *declRefExpr = dyn_cast(base->IgnoreCasts()); + declIdMapper.createResourceHeap(cast(declRefExpr->getDecl()), + resourceType); + } + // Buffer alias: emitting a load of the whole resource (runtime-array + // struct) produces invalid SPIR-V. emitError does not halt codegen, + // so return early to prevent the loadIfGLValue call below from + // emitting that invalid load. + // TODO: implement cross-function alias propagation for buffer aliases + // using VariablePointersStorageBuffer (tracked as follow-up). + else if (const auto *var = + dyn_cast_or_null(getReferencedDef(srcExpr))) { + if (descriptorHeapBufferAliasVars.count(var)) { + emitError("heap buffer alias cannot be returned from a function; " + "access the buffer element directly at the return site", + retVal->getLocStart()); + return; + } + } } auto *retInfo = loadIfGLValue(retVal); @@ -3154,10 +3177,14 @@ SpirvEmitter::tryToAssignToDescriptorHeapBuffer( return llvm::None; const Expr *rhsValue = assignExpr->getRHS()->IgnoreParenCasts(); - if (!isDescriptorHeap(rhsValue)) + if (!isHeapSourcedValue(rhsValue)) return llvm::None; - (void)doExpr(rhsValue); + // doExpr populates descriptorHeapBufferAccesses for the direct-subscript + // path; the VarDecl fallback bypasses that map, so skip the call to avoid + // emitting dead instructions when the source is an alias variable. + if (isDescriptorHeap(rhsValue)) + (void)doExpr(rhsValue); if (!tryToAssignDescriptorHeapBufferAlias(assignExpr->getLHS(), assignExpr->getRHS())) return llvm::None; @@ -3424,6 +3451,28 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) { // for it if it can act as out parameter. SpirvInstruction *argInfo = nullptr; if (const auto *declRefExpr = dyn_cast(arg)) { + // Heap buffer alias vars are not registered in astDecls, so + // getDeclEvalInfo would crash. Passing a buffer alias by value to a + // user function also requires VariablePointersStorageBuffer and callee + // parameter type changes that are not yet implemented; emit a diagnostic + // instead of crashing. TODO: implement full buffer-alias function-call + // support (VariablePointersStorageBuffer + matching createFnParam type). + const auto *var = dyn_cast(declRefExpr->getDecl()); + if (var && descriptorHeapBufferAliasVars.count(var)) { + emitError("heap buffer alias cannot be passed to a user function; " + "access the buffer element directly at the call site", + arg->getLocStart()); + // emitError does not halt codegen; returning nullptr here propagates + // to spvBuilder and causes an access violation before the diagnostic + // surfaces. Return a zero uint placeholder so downstream expression + // consumers remain valid. The emitted error ensures the shader is + // rejected even if codegen continues with the placeholder. + QualType retTy = callExpr->getCallReturnType(astContext); + if (retTy->isVoidType()) + return nullptr; + return spvBuilder.getConstantInt(astContext.UnsignedIntTy, + llvm::APInt(32, 0), false); + } argInfo = declIdMapper.getDeclEvalInfo(declRefExpr->getDecl(), arg->getLocStart()); } @@ -5197,7 +5246,7 @@ bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, !isAKindOfStructuredOrByteBuffer(dstType)) return false; - const bool srcIsHeap = isDescriptorHeap(srcExpr->IgnoreParenCasts()); + const bool srcIsHeap = isHeapSourcedValue(srcExpr->IgnoreParenCasts()); const bool wasHeap = descriptorHeapImageAliasVars.count(dstVar) || descriptorHeapBufferAliasVars.count(dstVar); const bool wasBound = stateIt != descriptorHeapVarState.end() && @@ -5241,8 +5290,31 @@ bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapImageAccesses.find(src); - if (found == descriptorHeapImageAccesses.end()) - return false; + if (found == descriptorHeapImageAccesses.end()) { + // Fallback: src is a DeclRefExpr referencing an existing image alias + // variable. The Expr*-keyed access map is only populated for direct heap + // subscripts; consult the VarDecl-keyed alias map instead. + const auto *srcVar = dyn_cast_or_null(getReferencedDef(src)); + if (!srcVar) + return false; + const auto srcAliasIt = descriptorHeapImageAliasVars.find(srcVar); + if (srcAliasIt == descriptorHeapImageAliasVars.end()) + return false; + const DescriptorHeapImageAlias &srcAlias = srcAliasIt->second; + DescriptorHeapImageAlias &dstAlias = descriptorHeapImageAliasVars[dstVar]; + if (!dstAlias.indexVar) + dstAlias.indexVar = createDescriptorHeapIndexVar(dstVar); + dstAlias.imageType = srcAlias.imageType; + dstAlias.arrayType = srcAlias.arrayType; + dstAlias.heap = srcAlias.heap; + // Propagate the runtime index: load current slot from the source alias + // variable and store it into the destination's index variable. + SpirvInstruction *index = spvBuilder.createLoad( + astContext.UnsignedIntTy, srcAlias.indexVar, srcExpr->getExprLoc()); + spvBuilder.createStore(dstAlias.indexVar, index, srcExpr->getExprLoc(), + srcExpr->getSourceRange()); + return true; + } auto &alias = descriptorHeapImageAliasVars[dstVar]; if (!alias.indexVar) @@ -5280,8 +5352,33 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapBufferAccesses.find(src); - if (found == descriptorHeapBufferAccesses.end()) - return false; + if (found == descriptorHeapBufferAccesses.end()) { + // Fallback: src is a DeclRefExpr referencing an existing buffer alias + // variable. The Expr*-keyed access map is only populated for direct heap + // subscripts; consult the VarDecl-keyed alias map instead. + const auto *srcVar = dyn_cast_or_null(getReferencedDef(src)); + if (!srcVar) + return false; + const auto srcAliasIt = descriptorHeapBufferAliasVars.find(srcVar); + if (srcAliasIt == descriptorHeapBufferAliasVars.end()) + return false; + const DescriptorHeapBufferAlias &srcAlias = srcAliasIt->second; + DescriptorHeapBufferAlias &dstAlias = descriptorHeapBufferAliasVars[dstVar]; + if (!dstAlias.indexVar) + dstAlias.indexVar = createDescriptorHeapIndexVar(dstVar); + dstAlias.bufferPointerType = srcAlias.bufferPointerType; + dstAlias.arrayType = srcAlias.arrayType; + dstAlias.heap = srcAlias.heap; + dstAlias.layoutRule = srcAlias.layoutRule; + dstAlias.counterUnsupported = isRWStructuredBuffer(dstVar->getType()); + // Propagate the runtime index: load current slot from the source alias + // variable and store it into the destination's index variable. + SpirvInstruction *index = spvBuilder.createLoad( + astContext.UnsignedIntTy, srcAlias.indexVar, srcExpr->getExprLoc()); + spvBuilder.createStore(dstAlias.indexVar, index, srcExpr->getExprLoc(), + srcExpr->getSourceRange()); + return true; + } auto &alias = descriptorHeapBufferAliasVars[dstVar]; if (!alias.indexVar) @@ -5494,7 +5591,7 @@ bool SpirvEmitter::tryToAssignCounterVar(const DeclaratorDecl *dstDecl, auto *srcCounter = getFinalACSBufferCounterInstruction(srcExpr); if (!srcCounter) { if (spirvOptions.useDescriptorHeap && - isDescriptorHeap(srcExpr->IgnoreParenCasts())) { + isHeapSourcedValue(srcExpr->IgnoreParenCasts())) { markDescriptorHeapCounterUnsupported(dstDecl); return true; } @@ -5538,7 +5635,7 @@ bool SpirvEmitter::tryToAssignCounterVar(const Expr *dstExpr, if ((dstCounter == nullptr) != (srcCounter == nullptr)) { if (spirvOptions.useDescriptorHeap && dstCounter && - isDescriptorHeap(srcExpr->IgnoreParenCasts())) { + isHeapSourcedValue(srcExpr->IgnoreParenCasts())) { markDescriptorHeapCounterUnsupported(getReferencedDef(dstExpr)); return true; } @@ -8423,7 +8520,7 @@ bool SpirvEmitter::isBufferTextureIndexing(const CXXOperatorCallExpr *indexExpr, return false; } -bool SpirvEmitter::isDescriptorHeap(const Expr *expr) { +bool SpirvEmitter::isDescriptorHeap(const Expr *expr) const { const CXXOperatorCallExpr *operatorExpr = dyn_cast(expr); if (!operatorExpr) return false; @@ -8438,6 +8535,16 @@ bool SpirvEmitter::isDescriptorHeap(const Expr *expr) { isSamplerDescriptorHeap(objectType); } +bool SpirvEmitter::isHeapSourcedValue(const Expr *expr) const { + if (isDescriptorHeap(expr->IgnoreParenCasts())) + return true; + const auto *var = dyn_cast_or_null(getReferencedDef(expr)); + if (!var) + return false; + return descriptorHeapImageAliasVars.count(var) || + descriptorHeapBufferAliasVars.count(var); +} + void SpirvEmitter::getDescriptorHeapOperands(const Expr *expr, const Expr **base, const Expr **index) { diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index ddc3caa18f..f6cce7b6e4 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -284,7 +284,16 @@ class SpirvEmitter : public ASTConsumer { const Expr **base = nullptr, const Expr **index = nullptr); - bool isDescriptorHeap(const Expr *expr); + bool isDescriptorHeap(const Expr *expr) const; + + /// Returns true if expr is heap-sourced: either a direct descriptor + /// heap subscript (ResourceDescriptorHeap[i]) or a DeclRefExpr referencing a + /// local variable that was previously assigned from a heap subscript and is + /// recorded in the image or buffer alias maps. + /// + /// Use this in place of bare isDescriptorHeap() at all sites that ask "is + /// this value heap-sourced?" so that alias-to-alias flows are recognized. + bool isHeapSourcedValue(const Expr *expr) const; void getDescriptorHeapOperands(const Expr *expr, const Expr **base, const Expr **index); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl new file mode 100644 index 0000000000..2bdb84cf87 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl @@ -0,0 +1,56 @@ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies that heap-alias image reads and writes survive function boundaries +// (both return and parameter directions) with the correct descriptor slot. +// +// returned from a function -> slot 40, OpImageRead/OpImageWrite +// passed as a parameter -> slot 41, OpImageRead/OpImageWrite +// +// Plain reads/writes work because the loaded handle is enough for spirv-opt to +// promote the copies. Atomics fail because the descriptor index does not cross +// OpFunctionCall — see alias-return.hlsl and image-alias-fn-param.hlsl. +// Keep this passing when fixing the atomic case. + +// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[RWTexType:[a-zA-Z0-9_]+]] = OpTypeImage %uint 2D 2 0 0 2 R32ui +// CHECK-DAG: %[[RWTexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexType]] + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant + +RWByteAddressBuffer outputBytes : register(u0); + +RWTexture2D getTex() { + RWTexture2D t = ResourceDescriptorHeap[40]; + return t; +} + +uint bump(RWTexture2D t, uint2 coord) { + uint v = t[coord]; + t[coord] = v + 1; + return v; +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + // Returned from a function: uses of the returned resource hit slot 40. + // CHECK: %[[RetDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_40 + // CHECK: %[[RetHandle:[a-zA-Z0-9_]+]] = OpLoad %[[RWTexType]] %[[RetDesc]] + // CHECK-NOT: OpStore {{.*}} %[[RetHandle]] + // CHECK: OpImageRead %v4uint %[[RetHandle]] + // CHECK: OpImageWrite %[[RetHandle]] + RWTexture2D returned = getTex(); + uint r0 = returned[tid.xy]; + returned[tid.xy] = r0 + 1; + + // Passed to a function: uses inside the callee hit slot 41. + // CHECK: %[[ParamDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_41 + // CHECK: %[[ParamHandle:[a-zA-Z0-9_]+]] = OpLoad %[[RWTexType]] %[[ParamDesc]] + // CHECK-NOT: OpStore {{.*}} %[[ParamHandle]] + // CHECK: OpImageRead %v4uint %[[ParamHandle]] + // CHECK: OpImageWrite %[[ParamHandle]] + RWTexture2D passed = ResourceDescriptorHeap[41]; + uint r1 = bump(passed, tid.xy); + + outputBytes.Store(0, r0); + outputBytes.Store(4, r1); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-return.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-return.hlsl new file mode 100644 index 0000000000..c2aa1a5ae3 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-return.hlsl @@ -0,0 +1,33 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// Verifies: returning a heap buffer alias from a function emits a clear +// diagnostic instead of producing invalid SPIR-V. +// +// Regression (B2 / C3): returning a StructuredBuffer alias variable caused +// loadIfGLValue to emit an OpLoad of the whole runtime-array struct through a +// StorageBuffer pointer, which is invalid SPIR-V. The fix emits an actionable +// error before the load is attempted. +// +// Cross-function alias propagation (returning the alias and using it in the +// caller) is not yet implemented; it requires VariablePointersStorageBuffer so +// that the OpBufferPointerEXT value can survive an OpReturnValue boundary. +// Tracked as a follow-up; until then this path emits an error. +// +// Image aliases (RWTexture2D) are handled by the legalization pipeline (System A) +// and are not covered by this test. + +// CHECK: heap buffer alias cannot be returned from a function + +RWByteAddressBuffer outputBytes : register(u0); + +StructuredBuffer makeAlias() { + StructuredBuffer a = ResourceDescriptorHeap[40]; + StructuredBuffer b = ResourceDescriptorHeap[41]; + b = a; + return b; +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + outputBytes.Store(0, makeAlias()[0]); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-to-alias.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-to-alias.hlsl new file mode 100644 index 0000000000..1773ebb6c6 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-to-alias.hlsl @@ -0,0 +1,60 @@ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// Verifies that alias-to-alias assignment (sb2 = sb1) and copy-init (sb3 = sb1) +// propagate the source's descriptor slot to the destination. +// +// Propagation is compile-time: tryToAssignDescriptorHeapBufferAlias copies the +// source's index SpirvInstruction into the destination alias; legalization then +// constant-folds the index-variable loads away. As a result the overwritten +// slot (%uint_31, %uint_21) appears only in the dead declaration-site access +// chain, never at a use site. A second occurrence means propagation failed. +// +// Regression: alias-var sources previously crashed in getDeclEvalInfo because +// buffer alias VarDecls are not registered in astDecls. + +struct Payload { uint value; }; +RWByteAddressBuffer outputBytes : register(u0); + +// ---- StructuredBuffer section ---------------------------------------- +// sb1->slot 30, sb2->slot 31 (overwritten to 30 by sb2=sb1), sb3 copy-init from sb1. +// +// sb2's dead declaration chain is the ONLY %uint_31 StructuredBuffer chain: +// CHECK: OpUntypedAccessChainKHR %type_untyped_pointer %_runtimearr_type_buffer_ext_0 %resource_heap %uint_31 +// CHECK-NOT: OpUntypedAccessChainKHR %type_untyped_pointer %_runtimearr_type_buffer_ext_0 %resource_heap %uint_31 +// +// sb2[0] and sb3[0] produce element loads (via propagated slot 30): +// CHECK: OpLoad %uint +// CHECK: OpLoad %uint + +// ---- ConstantBuffer section ------------------------------------------ +// cb1->slot 20, cb2->slot 21 (overwritten to 20 by cb2=cb1). +// +// cb2's dead declaration chain is the ONLY %uint_21 ConstantBuffer chain: +// CHECK: OpUntypedAccessChainKHR %type_untyped_pointer %_runtimearr_type_buffer_ext %resource_heap %uint_21 +// CHECK-NOT: OpUntypedAccessChainKHR %type_untyped_pointer %_runtimearr_type_buffer_ext %resource_heap %uint_21 +// +// cb2.value produces a member load (via propagated slot 20): +// CHECK: OpLoad %uint + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + uint r0, r1; + + StructuredBuffer sb1 = ResourceDescriptorHeap[30]; + StructuredBuffer sb2 = ResourceDescriptorHeap[31]; + sb2 = sb1; // alias assign: sb2 now uses sb1's slot (30) + + StructuredBuffer sb3 = sb1; // copy-init: sb3 uses sb1's slot (30) + + r0 = sb2[0]; // must use slot 30 + r0 += sb3[0]; // must use slot 30 + + ConstantBuffer cb1 = ResourceDescriptorHeap[20]; + ConstantBuffer cb2 = ResourceDescriptorHeap[21]; + cb2 = cb1; // alias assign: cb2 now uses cb1's slot (20) + + r1 = cb2.value; // must use slot 20 + + outputBytes.Store(0, r0); + outputBytes.Store(4, r1); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-alias-fn-param.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-alias-fn-param.error.hlsl new file mode 100644 index 0000000000..985f309c5d --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-alias-fn-param.error.hlsl @@ -0,0 +1,31 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_STRUCTURED %s 2>&1 | FileCheck --check-prefix=SB %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck --check-prefix=BAB %s + +// Verifies that passing a heap buffer alias to a user function emits a +// diagnostic instead of crashing (buffer alias VarDecls are not in astDecls, +// so getDeclEvalInfo would fault). +// +// TODO: remove once VariablePointersStorageBuffer-based pass-by-value is +// implemented and buffer aliases can be passed to functions. + +// SB: heap buffer alias cannot be passed to a user function +// BAB: heap buffer alias cannot be passed to a user function + +RWByteAddressBuffer outputBytes : register(u0); + +#ifdef TEST_STRUCTURED +uint consume(StructuredBuffer buf) { return buf[0]; } +#else +uint consume(ByteAddressBuffer buf) { return buf.Load(0); } +#endif + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { +#ifdef TEST_STRUCTURED + StructuredBuffer sb = ResourceDescriptorHeap[0]; + outputBytes.Store(0, consume(sb)); +#else + ByteAddressBuffer bab = ResourceDescriptorHeap[0]; + outputBytes.Store(0, consume(bab)); +#endif +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl new file mode 100644 index 0000000000..c6c57c12c9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl @@ -0,0 +1,41 @@ +// XFAIL: * +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s + +// XFAIL: heap image alias passed to a function and used in an atomic. +// Same failure as alias-return.hlsl: [VUID-StandaloneSpirv-OpTypeImage-06924]. +// +// The argument is copied into a Function-class "param.var.*" variable. The +// callee has no entry in the image alias map for its parameter, so the atomic +// falls back to OpImageTexelPointer on the parameter, pinning it as a variable +// and keeping the image-typed OpStore alive through validation. +// +// Plain reads/writes through the same parameter work — see alias-fn-readwrite.hlsl. +// Note: function-params.hlsl only checks the pre-legalization form (-fcgl), +// where the validator relaxes this rule, so it does not catch this failure. +// +// TODO: propagate descriptor index across function boundaries, then remove XFAIL. + +// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[RWTexType:[a-zA-Z0-9_]+]] = OpTypeImage %uint 2D 2 0 0 2 R32ui +// CHECK-DAG: %[[RWTexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexType]] +// CHECK-DAG: %[[UntypedImage:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR Image + +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant + +RWByteAddressBuffer outputBytes : register(u0); + +void bump(RWTexture2D t, uint2 coord, out uint orig) { + // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_40 + // CHECK-NOT: OpImageTexelPointer + // CHECK: %[[TexelPtr:[a-zA-Z0-9_]+]] = OpUntypedImageTexelPointerEXT %[[UntypedImage]] %[[RWTexType]] %[[Desc]] + // CHECK: OpAtomicIAdd %uint %[[TexelPtr]] + InterlockedAdd(t[coord], 1, orig); +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture2D tex = ResourceDescriptorHeap[40]; + uint r; + bump(tex, tid.xy, r); + outputBytes.Store(0, r); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl index ce0a3f0878..bb765be87f 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.mixed-alias.error.hlsl @@ -4,6 +4,7 @@ // RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=3 %s 2>&1 | FileCheck %s // RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=4 %s 2>&1 | FileCheck %s // RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=5 %s 2>&1 | FileCheck %s +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=6 %s | FileCheck %s --check-prefix=OK // Verifies that a local resource variable assigned from both a bound resource // and ResourceDescriptorHeap is rejected. @@ -26,6 +27,9 @@ // CASE=3, CASE=5) are tracked in descriptorHeapVarState; heap-then-bound // assignments (CASE=2, CASE=4) are detected by observing that the alias map // already contains an entry for the variable. +// +// CASE=6 (regression): heap-to-heap alias copy must NOT trigger the mixing +// diagnostic; only a bound source mixed with a heap destination is true mixing. // CHECK: error: {{.*}}mixing bound and descriptor heap resources in the same variable is not supported with SPV_EXT_descriptor_heap // OK: OpUntypedImageTexelPointerEXT @@ -86,6 +90,15 @@ void main(uint3 tid : SV_DispatchThreadID) { mixedBuf = ResourceDescriptorHeap[3]; original = mixedBuf.Load(0); +#elif CASE == 6 + // Heap-to-heap alias copy: both sources are heap-backed, so this is NOT + // mixing. heapB = heapA must propagate heapA's descriptor index to heapB + // without emitting the mixing error. + RWTexture2D heapA = ResourceDescriptorHeap[1]; + RWTexture2D heapB = ResourceDescriptorHeap[2]; + heapB = heapA; + InterlockedAdd(heapB[tid.xy], 6, original); + #endif outputBytes.Store(0, original); From 58213f5d8ea658db25c964c2c48f307b97a2c495 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Sun, 16 Aug 2026 11:57:18 -0700 Subject: [PATCH 11/45] Remove useless scope block in tryToAssignDescriptorHeapImageAlias --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index aa6927e73c..a20c9ade67 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5281,12 +5281,10 @@ bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( (!isRWTexture(dstVar->getType()) && !isRWBuffer(dstVar->getType()))) return false; - { - auto stateIt = descriptorHeapVarState.find(dstVar); - if (stateIt != descriptorHeapVarState.end() && - stateIt->second == DescriptorHeapVarState::Mixed) - return false; - } + auto stateIt = descriptorHeapVarState.find(dstVar); + if (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Mixed) + return false; const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapImageAccesses.find(src); From c82a49d5f190774c664c2b53e44fcbeb9856dd5a Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Sun, 16 Aug 2026 11:57:50 -0700 Subject: [PATCH 12/45] Remove dead append/consume guard in incDecRWACSBufferCounter --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index a20c9ade67..eb5758781a 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5527,21 +5527,6 @@ SpirvEmitter::incDecRWACSBufferCounter(const CXXMemberCallExpr *expr, return nullptr; } - // Only heap-loaded append/consume buffers are unsupported. Explicitly bound - // ones keep working in native heap mode, so key the diagnostic on the object - // actually having a heap alias rather than on the option alone. - if (spirvOptions.useDescriptorHeap && - (isAppendStructuredBuffer(object->getType()) || - isConsumeStructuredBuffer(object->getType()))) { - const auto *objVar = dyn_cast_or_null(getReferencedDef(object)); - if (objVar && descriptorHeapBufferAliasVars.count(objVar)) { - emitError("append/consume structured buffers are not supported with " - "SPV_EXT_descriptor_heap", - expr->getCallee()->getExprLoc()); - return nullptr; - } - } - auto *counter = getFinalACSBufferCounterInstruction(object); if (!counter) { emitFatalError("Cannot access associated counter variable for an array of " From 3f5c342461cd3111d9e88b71478532ae8a83f1be Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Sun, 16 Aug 2026 11:58:31 -0700 Subject: [PATCH 13/45] Remove unreachable !decl guard in heap index codegen path --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index eb5758781a..10524a6494 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -7145,18 +7145,10 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, return nullptr; } QualType resourceType = parentExpr->getType(); - // The heap object must be a direct reference to the builtin heap - // variable. Anything else (e.g. a non-variable expression) has no backing - // VarDecl. - const auto *declRefExpr = dyn_cast(baseExpr->IgnoreCasts()); + // The heap object is always a direct DeclRefExpr to the builtin heap + // VarDecl; sema rejects any other form. const auto *decl = - declRefExpr ? dyn_cast(declRefExpr->getDecl()) : nullptr; - if (!decl) { - emitError("unsupported ResourceDescriptorHeap/SamplerDescriptorHeap " - "expression", - baseExpr->getExprLoc()); - return nullptr; - } + cast(cast(baseExpr->IgnoreCasts())->getDecl()); SpirvVariableLike *var = declIdMapper.createResourceHeap(decl, resourceType); From 9d74f7debd199474458057b0c0394646dae3fbc5 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Sun, 16 Aug 2026 11:59:30 -0700 Subject: [PATCH 14/45] Propagate requestTargetEnv failure for desc heap CapabilityVisitor::visitInstructionAllTypes was dropping the bool return of requestTargetEnv, so a vk<1.3 target-env would emit the diagnostic but keep lowering. Added sm6_6.descriptorheap.ext.targetenv.error.hlsl to cover rejection of -fspv-use-descriptor-heap with -fspv-target-env=vulkan1.2. --- tools/clang/lib/SPIRV/CapabilityVisitor.cpp | 3 ++- .../sm6_6.descriptorheap.ext.targetenv.error.hlsl | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.targetenv.error.hlsl diff --git a/tools/clang/lib/SPIRV/CapabilityVisitor.cpp b/tools/clang/lib/SPIRV/CapabilityVisitor.cpp index c48f898a28..f79c455319 100644 --- a/tools/clang/lib/SPIRV/CapabilityVisitor.cpp +++ b/tools/clang/lib/SPIRV/CapabilityVisitor.cpp @@ -956,7 +956,8 @@ bool CapabilityVisitor::visit(SpirvModule *, Visitor::Phase phase) { if (spvOptions.useDescriptorHeap) { const llvm::StringRef feature = "DescriptorHeap"; - featureManager.requestTargetEnv(SPV_ENV_VULKAN_1_3, feature, {}); + if (!featureManager.requestTargetEnv(SPV_ENV_VULKAN_1_3, feature, {})) + return false; addExtension(Extension::EXT_descriptor_heap, feature, {}); addExtension(Extension::KHR_untyped_pointers, feature, {}); addCapability(spv::Capability::DescriptorHeapEXT); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.targetenv.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.targetenv.error.hlsl new file mode 100644 index 0000000000..cfd6d056ad --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.targetenv.error.hlsl @@ -0,0 +1,12 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.2 -spirv %s 2>&1 | FileCheck %s + +// Verifies that -fspv-use-descriptor-heap rejects target environments below +// Vulkan 1.3 with a clear diagnostic. + +// CHECK: error: Vulkan 1.3 is required for DescriptorHeap but not permitted to use + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + Texture2D tex = ResourceDescriptorHeap[tid.x]; + (void)tex; +} From 478c57cbe6c1b1a3f5180b8383f67b4609f2afd7 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Sun, 16 Aug 2026 12:40:00 -0700 Subject: [PATCH 15/45] [SPIR-V] Add release note for SPV_EXT_descriptor_heap native lowering (#8517) --- docs/ReleaseNotes.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 19c0d75174..d457ee3301 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -19,6 +19,13 @@ The included licenses apply to the following files: ### Upcoming Release +#### SPIR-V + +- Added native `SPV_EXT_descriptor_heap` lowering for `ResourceDescriptorHeap` + and `SamplerDescriptorHeap` via `-fspv-use-descriptor-heap`. Requires + `-fspv-target-env=vulkan1.3` + [#8517](https://github.com/microsoft/DirectXShaderCompiler/pull/8517). + Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. From e5f7a619e092ac799802ddd934eaa726b7a68399 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Sun, 16 Aug 2026 14:55:20 -0700 Subject: [PATCH 16/45] nit fixes --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 10524a6494..7892280f83 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2031,8 +2031,7 @@ bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, // bypasses that map, so skip the call to avoid emitting dead instructions. if (isDescriptorHeap(init->IgnoreParenCasts())) (void)doExpr(init->IgnoreParenCasts()); - tryToAssignDescriptorHeapBufferAlias(decl, init); - return true; + return tryToAssignDescriptorHeapBufferAlias(decl, init); } return false; @@ -2994,8 +2993,9 @@ void SpirvEmitter::doReturnStmt(const ReturnStmt *stmt) { } // Buffer alias: emitting a load of the whole resource (runtime-array // struct) produces invalid SPIR-V. emitError does not halt codegen, - // so return early to prevent the loadIfGLValue call below from - // emitting that invalid load. + // so terminate the basic block with an undef return value before + // returning to prevent loadIfGLValue from emitting an invalid load and + // leaving the block without a terminator. // TODO: implement cross-function alias propagation for buffer aliases // using VariablePointersStorageBuffer (tracked as follow-up). else if (const auto *var = @@ -3004,6 +3004,9 @@ void SpirvEmitter::doReturnStmt(const ReturnStmt *stmt) { emitError("heap buffer alias cannot be returned from a function; " "access the buffer element directly at the return site", retVal->getLocStart()); + spvBuilder.createReturnValue( + spvBuilder.getUndef(curFunction->getReturnType()), + stmt->getReturnLoc()); return; } } @@ -3464,14 +3467,13 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) { arg->getLocStart()); // emitError does not halt codegen; returning nullptr here propagates // to spvBuilder and causes an access violation before the diagnostic - // surfaces. Return a zero uint placeholder so downstream expression + // surfaces. Return a typed undef placeholder so downstream expression // consumers remain valid. The emitted error ensures the shader is // rejected even if codegen continues with the placeholder. QualType retTy = callExpr->getCallReturnType(astContext); if (retTy->isVoidType()) return nullptr; - return spvBuilder.getConstantInt(astContext.UnsignedIntTy, - llvm::APInt(32, 0), false); + return spvBuilder.getUndef(retTy); } argInfo = declIdMapper.getDeclEvalInfo(declRefExpr->getDecl(), arg->getLocStart()); @@ -5171,8 +5173,8 @@ SpirvEmitter::processStructuredBufferLoad(const CXXMemberCallExpr *expr) { // (Verified required: scoping this to alias vars only regresses the direct // heap-access tests; non-heap callers are unaffected in the existing suite.) if (result && !result->isRValue()) { - result = - spvBuilder.createLoad(structType, result, buffer->getExprLoc(), range); + result = spvBuilder.createLoad(expr->getType(), result, + buffer->getExprLoc(), range); } return result; From 1f5a41ab3b44eb6e0f8abe62ee16658a2bff87ea Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 21 Aug 2026 10:17:06 -0700 Subject: [PATCH 17/45] Fix tests and test formatting --- ...sm6_6.descriptorheap.ext.array-stride.hlsl | 9 ++-- .../sm6_6.descriptorheap.ext.buffer.hlsl | 9 ++-- .../sm6_6.descriptorheap.ext.gather.hlsl | 8 ++- ...6_6.descriptorheap.ext.rwtexture-dims.hlsl | 8 +-- ...m6_6.descriptorheap.ext.static-global.hlsl | 54 ++++++++++--------- ...sm6_6.descriptorheap.ext.texture-dims.hlsl | 8 +-- .../sm6_6.descriptorheap.ext.texture-ms.hlsl | 4 +- .../sm6_6.descriptorheap.ext.texture.hlsl | 4 +- .../sm6_6.descriptorheap.ext.texturecube.hlsl | 6 +-- ...m6_6.descriptorheap.ext.typed-formats.hlsl | 8 +-- 10 files changed, 64 insertions(+), 54 deletions(-) diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl index e690564fb8..d3722a3a45 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.hlsl @@ -66,10 +66,11 @@ // CHECK-DAG: OpDecorateId %[[SamplerArray]] ArrayStrideIdEXT %[[SampSize]] // Three distinct OpConstantSizeOfEXT calls exist: ImgPlaceholder, Uniform -// buffer, and sampler. Regression test: no per-element or per-access behavior; -// otherwise StorageBuffer would add a fourth, pushing the count above three. -// COUNT-COUNT-3: OpConstantSizeOfEXT %uint -// COUNT-NOT: OpConstantSizeOfEXT %uint +// buffer, and sampler, each asserted by a CHECK-DAG above. Regression test: no +// per-element or per-access behavior; otherwise the StorageBuffer descriptor +// would be measured too, adding a fourth query. +// COUNT-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer{{$}} +// COUNT-NOT: OpConstantSizeOfEXT %uint %[[SBBufDesc]] struct Constants { uint value; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl index eb0b6b25ca..6c178f5e6c 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer.hlsl @@ -12,10 +12,11 @@ // reassignment -> re-indexes shared StorageBuffer array (uint_4) -> same %type_StructuredBuffer_* pointer // CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant -// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer -// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]] -// CHECK-DAG: %[[UBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform -// CHECK-DAG: %[[UBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[UBufDesc]] +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer{{$}} +// CHECK-DAG: %[[SBBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]]{{$}} +// CHECK-DAG: %[[UBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform{{$}} +// CHECK-DAG: %[[UBufArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[UBufDesc]]{{$}} + // Anchor each pointer type to its struct-type prefix to prevent FileCheck from // binding all three StorageBuffer captures to the scalar %_ptr_StorageBuffer_uint. // CHECK-DAG: %[[SBInputPtr:[a-zA-Z0-9_]+]] = OpTypePointer StorageBuffer %type_StructuredBuffer_{{.*}} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl index 9b1a720201..d3efccdeb7 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.gather.hlsl @@ -11,8 +11,12 @@ // CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} // CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} -// CHECK-DAG: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant -// CHECK-DAG: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// The two heap variables have identical definitions apart from their result id, +// so bind each id from its OpName before checking the definition. +// CHECK-DAG: OpName %[[ResourceHeap:[a-zA-Z0-9_]+]] "resource_heap" +// CHECK-DAG: OpName %[[SamplerHeap:[a-zA-Z0-9_]+]] "sampler_heap" +// CHECK-DAG: %[[ResourceHeap]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK-DAG: %[[SamplerHeap]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant float4 main(float2 uv : TEXCOORD0) : SV_Target { Texture2D tex = ResourceDescriptorHeap[0]; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl index c44da5b066..c84fa6de81 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.rwtexture-dims.hlsl @@ -21,10 +21,10 @@ // All RWTextures are resource descriptors, so every array shares one resource stride // (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) -// CHECK-DAG: OpDecorateId %[[RA_RW1D]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] -// CHECK-DAG: OpDecorateId %[[RA_RW1DArr]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_RW2DArr]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_RW3D]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW1D]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_RW1DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW2DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RW3D]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl index e12cf41076..04ad1d4812 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.static-global.hlsl @@ -5,19 +5,23 @@ // Texture2D / StructuredBuffer / RWTexture2D, plus a literal-index // SamplerState. -// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant -// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown -// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler -// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer -// CHECK-DAG: %[[RWTex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 2 Rgba32f +// CHECK-DAG: %[[UntypedPtr:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler +// CHECK-DAG: %[[SBBufDesc:[a-zA-Z0-9_]+]] = OpTypeBufferEXT StorageBuffer +// CHECK-DAG: %[[RWTex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 2 Rgba32f -// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} -// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} -// CHECK-DAG: %[[RA_SBBuf:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]]{{$}} -// CHECK-DAG: %[[RA_RWTex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTex2DType]]{{$}} +// CHECK-DAG: %[[RA_Tex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]]{{$}} +// CHECK-DAG: %[[RA_Sampler:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]]{{$}} +// CHECK-DAG: %[[RA_SBBuf:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SBBufDesc]]{{$}} +// CHECK-DAG: %[[RA_RWTex2D:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTex2DType]]{{$}} -// CHECK-DAG: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant -// CHECK-DAG: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// The two heap variables have identical definitions apart from their result id, +// so bind each id from its OpName before checking the definition. +// CHECK-DAG: OpName %[[ResourceHeap:[a-zA-Z0-9_]+]] "resource_heap" +// CHECK-DAG: OpName %[[SamplerHeap:[a-zA-Z0-9_]+]] "sampler_heap" +// CHECK-DAG: %[[ResourceHeap]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant +// CHECK-DAG: %[[SamplerHeap]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant // Runtime uint indices (placed in $Globals cbuffer by DXC). uint BindlessSRV_ColorTex; @@ -35,30 +39,30 @@ static const SamplerState BilinearSamp = SamplerDescriptorHeap[2]; [numthreads(8, 8, 1)] void main(uint3 tid : SV_DispatchThreadID) { // Heap indices are dynamic (loaded from $Globals), not literal constants. - // CHECK: %[[TexIdx:[a-zA-Z0-9_]+]] = OpLoad %uint - // CHECK: %[[TexDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap]] %[[TexIdx]] - // CHECK: OpLoad %[[Tex2DType]] %[[TexDesc]] + // CHECK: %[[TexIdx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: %[[TexDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Tex2D]] %[[ResourceHeap]] %[[TexIdx]] + // CHECK: OpLoad %[[Tex2DType]] %[[TexDesc]] - // CHECK: %[[BufIdx:[a-zA-Z0-9_]+]] = OpLoad %uint - // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_SBBuf]] %[[ResourceHeap]] %[[BufIdx]] - // CHECK: OpBufferPointerEXT + // CHECK: %[[BufIdx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_SBBuf]] %[[ResourceHeap]] %[[BufIdx]] + // CHECK: OpBufferPointerEXT - // CHECK: %[[OutIdx:[a-zA-Z0-9_]+]] = OpLoad %uint - // CHECK: %[[OutDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RWTex2D]] %[[ResourceHeap]] %[[OutIdx]] - // CHECK: OpLoad %[[RWTex2DType]] %[[OutDesc]] + // CHECK: %[[OutIdx:[a-zA-Z0-9_]+]] = OpLoad %uint + // CHECK: %[[OutDesc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_RWTex2D]] %[[ResourceHeap]] %[[OutIdx]] + // CHECK: OpLoad %[[RWTex2DType]] %[[OutDesc]] // Literal sampler index. - // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_2 - // CHECK: OpLoad %[[SamplerType]] + // CHECK: OpUntypedAccessChainKHR %[[UntypedPtr]] %[[RA_Sampler]] %[[SamplerHeap]] %uint_2 + // CHECK: OpLoad %[[SamplerType]] float2 uv = float2(tid.xy) / 512.0; - // CHECK: OpSampledImage - // CHECK: OpImageSampleExplicitLod + // CHECK: OpSampledImage + // CHECK: OpImageSampleExplicitLod float4 color = ColorTex.SampleLevel(BilinearSamp, uv, 0); float4 data = DataBuf[tid.x]; - // CHECK: OpImageWrite %{{[a-zA-Z0-9_]+}} + // CHECK: OpImageWrite %{{[a-zA-Z0-9_]+}} OutTex[tid.xy] = color + data; } diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl index c489f08d0c..94705bd367 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-dims.hlsl @@ -21,10 +21,10 @@ // All textures are resource descriptors, so every array shares one resource stride // (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) -// CHECK-DAG: OpDecorateId %[[RA_Tex1D]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] -// CHECK-DAG: OpDecorateId %[[RA_Tex1DArr]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_Tex2DArr]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_Tex3D]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex1D]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_Tex1DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex2DArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Tex3D]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl index d99312409f..cf342cb076 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture-ms.hlsl @@ -12,8 +12,8 @@ // Both multisampled textures are resource descriptors, so they share one resource stride // (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) -// CHECK-DAG: OpDecorateId %[[RA_MS]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] -// CHECK-DAG: OpDecorateId %[[RA_MSArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_MS]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_MSArr]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl index e83e13be26..5328d915cd 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texture.hlsl @@ -16,8 +16,8 @@ // Buffer and RWBuffer are both resource descriptors, so both arrays share one resource stride // (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) -// CHECK-DAG: OpDecorateId %[[RA_BufferType]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] -// CHECK-DAG: OpDecorateId %[[RA_RWBufferType]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_BufferType]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_RWBufferType]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl index 19c168ff5f..c60e927a36 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.texturecube.hlsl @@ -15,9 +15,9 @@ // Both cube arrays are resource descriptors and share one resource stride; the sampler array uses the separate sampler stride // (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) -// CHECK-DAG: OpDecorateId %[[RA_Cube]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] -// CHECK-DAG: OpDecorateId %[[RA_CubeArr]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_Sampler]] ArrayStrideIdEXT %[[SampSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_Cube]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_CubeArr]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_Sampler]] ArrayStrideIdEXT %[[SampSize:[a-zA-Z0-9_]+]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant // CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl index 67bad07c47..34dc1073be 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.typed-formats.hlsl @@ -25,10 +25,10 @@ // All of these are resource descriptors, so every array shares one resource stride // (derivation covered by sm6_6.descriptorheap.ext.array-stride.hlsl) -// CHECK-DAG: OpDecorateId %[[RA_TexUint]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] -// CHECK-DAG: OpDecorateId %[[RA_RWTexF2]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_RWTexU2]] ArrayStrideIdEXT %[[ResSize]] -// CHECK-DAG: OpDecorateId %[[RA_RWTexI]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_TexUint]] ArrayStrideIdEXT %[[ResSize:[a-zA-Z0-9_]+]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexF2]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexU2]] ArrayStrideIdEXT %[[ResSize]] +// CHECK-DAG: OpDecorateId %[[RA_RWTexI]] ArrayStrideIdEXT %[[ResSize]] // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtr]] UniformConstant From 5121b4ba81419c234fa9282f928068eb7816eca3 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 10 Sep 2026 15:36:04 -0700 Subject: [PATCH 18/45] improved comments and convention compliance --- .../clang/include/clang/SPIRV/SpirvBuilder.h | 14 ++-- tools/clang/include/clang/SPIRV/SpirvType.h | 4 +- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 6 -- tools/clang/lib/SPIRV/DeclResultIdMapper.h | 3 - tools/clang/lib/SPIRV/EmitVisitor.cpp | 3 +- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 23 +++--- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 75 ++++++++----------- tools/clang/lib/SPIRV/SpirvEmitter.h | 45 +++++------ 8 files changed, 70 insertions(+), 103 deletions(-) diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 20b6ff2d9b..6a2f3751fb 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -831,10 +831,9 @@ class SpirvBuilder { bool specConst = false); SpirvConstant *getConstantNull(QualType); SpirvConstant *getConstantString(llvm::StringRef str, bool specConst = false); - /// \brief Returns the OpConstantSizeOfEXT (SPV_EXT_descriptor_heap) for the - /// given descriptor operandType, yielding its client-API size in bytes as - /// a 32-bit unsigned value. The result is cached per operand type, so each - /// descriptor type emits at most one instruction. + /// \brief Returns the OpConstantSizeOfEXT (SPV_EXT_descriptor_heap) for descriptor operandType: + /// client-API size in bytes as a 32-bit unsigned integer. Cached per operand + /// type. SpirvConstant *getConstantSizeOfEXT(const SpirvType *operandType); SpirvSpecConstantTernaryOp * @@ -967,12 +966,11 @@ class SpirvBuilder { llvm::SmallVector builtinVars; /// Cache of OpConstantSizeOfEXT instructions keyed on the descriptor operand - /// type, so each distinct descriptor type emits at most one instruction. + /// type; each distinct descriptor type produces at most one constant. llvm::DenseMap constantSizeOfEXTMap; - /// Cached shared descriptor-heap array strides (SPV_EXT_descriptor_heap), so - /// each is emitted once per module (see - /// get{Resource,Sampler}HeapArrayStride). + /// Cached shared descriptor-heap array strides, each emitted once per + /// module (see get{Resource,Sampler}HeapArrayStride). SpirvInstruction *resourceHeapArrayStride = nullptr; SpirvInstruction *samplerHeapArrayStride = nullptr; diff --git a/tools/clang/include/clang/SPIRV/SpirvType.h b/tools/clang/include/clang/SPIRV/SpirvType.h index 6c71d4e1fd..9a9b730b1e 100644 --- a/tools/clang/include/clang/SPIRV/SpirvType.h +++ b/tools/clang/include/clang/SPIRV/SpirvType.h @@ -292,8 +292,8 @@ class RuntimeArrayType : public SpirvType { const SpirvType *getElementType() const { return elementType; } llvm::Optional getStride() const { return stride; } // When non-null, the array is decorated with ArrayStrideIdEXT referencing - // this constant (SPV_EXT_descriptor_heap) instead of a literal - // ArrayStride. Mutually exclusive with a literal stride. + // this constant instead of a literal ArrayStride. Mutually exclusive + // with a literal stride. SpirvInstruction *getArrayStrideId() const { return arrayStrideId; } private: diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index f712a10223..3a88334ffc 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -1136,12 +1136,6 @@ DeclResultIdMapper::createFnVar(const VarDecl *var, return varInstr; } -void DeclResultIdMapper::registerFnVarAlias(const VarDecl *var, - SpirvInstruction *varInstr) { - if (varInstr) - registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); -} - SpirvDebugGlobalVariable *DeclResultIdMapper::createDebugGlobalVariable( SpirvVariable *var, const QualType &type, const SourceLocation &loc, const StringRef &name) { diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.h b/tools/clang/lib/SPIRV/DeclResultIdMapper.h index b7730bb9f3..6a96ae8f45 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.h +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.h @@ -285,9 +285,6 @@ class DeclResultIdMapper { SpirvVariable *createFnVar(const VarDecl *var, llvm::Optional init); - /// \brief Registers a function-scope alias to an existing instruction. - void registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr); - /// \brief Creates a file-scope variable and returns its instruction. SpirvVariable *createFileVar(const VarDecl *var, llvm::Optional init); diff --git a/tools/clang/lib/SPIRV/EmitVisitor.cpp b/tools/clang/lib/SPIRV/EmitVisitor.cpp index aebe48180a..74b92939eb 100644 --- a/tools/clang/lib/SPIRV/EmitVisitor.cpp +++ b/tools/clang/lib/SPIRV/EmitVisitor.cpp @@ -2738,9 +2738,8 @@ uint32_t EmitTypeHandler::emitType(const SpirvType *type) { emitDecoration(id, spv::Decoration::ArrayStrideIdEXT, {getOrAssignResultId(strideId)}, llvm::None, /*usesIdParams=*/true); - } else if (llvm::Optional stride = raType->getStride()) { + } else if (llvm::Optional stride = raType->getStride()) emitDecoration(id, spv::Decoration::ArrayStride, {stride.getValue()}); - } } // NodePayloadArray types else if (const auto *npaType = dyn_cast(type)) { diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index 4478aa27c0..4ccb03544d 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2052,19 +2052,16 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { if (resourceHeapArrayStride) return resourceHeapArrayStride; - // The HLSL SM6.6 ResourceDescriptorHeap is a single flat array in which the - // client may place any resource descriptor at any slot. To match DX12 - // semantics, all resource descriptor arrays must share one stride equal to - // the largest resource descriptor size: max(sizeof(image), sizeof(buffer)). - // Images and buffers are the two resource descriptor categories defined by - // VkPhysicalDeviceDescriptorHeapPropertiesEXT (imageDescriptorSize / - // bufferDescriptorSize); textures lower to OpTypeImage, so image/buffer - // covers all relevant HLSL resource kinds. - // Both sizes are driver defined and known only at pipeline creation time, so - // the maximum is computed with OpSpecConstantOp over two OpConstantSizeOfEXT - // placeholders. A canonical sampled 2D float image and a Uniform buffer stand - // in as representatives; VkPhysicalDeviceDescriptorHeapPropertiesEXT reports - // one size per category, so subtype and storage class do not affect the size. + // ResourceDescriptorHeap is a flat array: any descriptor may sit at any slot. + // DX12 semantics require all resource arrays share stride = + // max(sizeof(image), sizeof(buffer)). + // VkPhysicalDeviceDescriptorHeapPropertiesEXT reports one size per category + // (imageDescriptorSize / bufferDescriptorSize); textures lower to + // OpTypeImage, so image/buffer covers all relevant HLSL resource kinds. Sizes are + // driver-defined (known only at pipeline creation), so the max is computed + // via OpSpecConstantOp over two OpConstantSizeOfEXT placeholders. A canonical + // sampled 2D float image and a Uniform buffer serve as representatives; + // subtype and storage class do not affect the category size. const SpirvType *placeholderImage = context.getImageType( context.getFloatType(32), spv::Dim::Dim2D, ImageType::WithDepth::No, /*arrayed*/ false, /*ms*/ false, ImageType::WithSampler::Yes, diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 7892280f83..99758b1854 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2991,11 +2991,9 @@ void SpirvEmitter::doReturnStmt(const ReturnStmt *stmt) { declIdMapper.createResourceHeap(cast(declRefExpr->getDecl()), resourceType); } - // Buffer alias: emitting a load of the whole resource (runtime-array - // struct) produces invalid SPIR-V. emitError does not halt codegen, - // so terminate the basic block with an undef return value before - // returning to prevent loadIfGLValue from emitting an invalid load and - // leaving the block without a terminator. + // Buffer alias: loading the whole runtime-array struct produces invalid + // SPIR-V. emitError does not halt codegen; terminate the block with an + // undef return to prevent loadIfGLValue emitting an unterminated block. // TODO: implement cross-function alias propagation for buffer aliases // using VariablePointersStorageBuffer (tracked as follow-up). else if (const auto *var = @@ -3454,22 +3452,20 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) { // for it if it can act as out parameter. SpirvInstruction *argInfo = nullptr; if (const auto *declRefExpr = dyn_cast(arg)) { - // Heap buffer alias vars are not registered in astDecls, so - // getDeclEvalInfo would crash. Passing a buffer alias by value to a - // user function also requires VariablePointersStorageBuffer and callee - // parameter type changes that are not yet implemented; emit a diagnostic - // instead of crashing. TODO: implement full buffer-alias function-call - // support (VariablePointersStorageBuffer + matching createFnParam type). + // Buffer alias vars are not in astDecls (getDeclEvalInfo would crash); + // passing by value also requires VariablePointersStorageBuffer + callee + // parameter type changes (not yet implemented). Emit diagnostic instead. + // TODO: implement full buffer-alias function-call support + // (VariablePointersStorageBuffer + matching createFnParam type). const auto *var = dyn_cast(declRefExpr->getDecl()); if (var && descriptorHeapBufferAliasVars.count(var)) { emitError("heap buffer alias cannot be passed to a user function; " "access the buffer element directly at the call site", arg->getLocStart()); - // emitError does not halt codegen; returning nullptr here propagates - // to spvBuilder and causes an access violation before the diagnostic - // surfaces. Return a typed undef placeholder so downstream expression - // consumers remain valid. The emitted error ensures the shader is - // rejected even if codegen continues with the placeholder. + // emitError does not halt codegen; nullptr propagates to spvBuilder + // causing an access violation. Return a typed undef so downstream + // consumers remain valid; the diagnostic rejects the shader even if + // codegen continues with the placeholder. QualType retTy = callExpr->getCallReturnType(astContext); if (retTy->isVoidType()) return nullptr; @@ -5166,12 +5162,8 @@ SpirvEmitter::processStructuredBufferLoad(const CXXMemberCallExpr *expr) { {zero, index}, buffer->getExprLoc(), range); // derefOrCreatePointerToValue returns an lvalue (AccessChain) when the base - // is an lvalue. This covers descriptor-heap buffers reached either directly - // (ResourceDescriptorHeap[i].Load()) or through a local alias var. - // StructuredBuffer::Load semantically returns a value, and the AST emits no - // LValueToRValue cast for the call expression, so emit the load explicitly. - // (Verified required: scoping this to alias vars only regresses the direct - // heap-access tests; non-heap callers are unaffected in the existing suite.) + // is an lvalue (SPIR-V pointer into the buffer element) not the value. + // StructuredBuffer::Load must return the element value, so load explicitly. if (result && !result->isRValue()) { result = spvBuilder.createLoad(expr->getType(), result, buffer->getExprLoc(), range); @@ -5260,10 +5252,9 @@ bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, emitError("mixing bound and descriptor heap resources in the same variable " "is not supported with SPV_EXT_descriptor_heap", loc); - // Leave any recorded alias in place. For heap-initialized buffer variables, - // doVarDecl returns early before calling createFnVar, so the alias is the - // only handle they have. Erasing it would crash downstream uses; keeping it - // is safe because the emitted error already fails the compilation. + // Leave the alias in place: heap-initialized buffer vars have doVarDecl + // return early (before createFnVar), making the alias their only handle; + // erasing it crashes downstream. The emitted error fails the compilation. descriptorHeapVarState[dstVar] = DescriptorHeapVarState::Mixed; return true; } @@ -5316,7 +5307,7 @@ bool SpirvEmitter::tryToAssignDescriptorHeapImageAlias( return true; } - auto &alias = descriptorHeapImageAliasVars[dstVar]; + DescriptorHeapImageAlias &alias = descriptorHeapImageAliasVars[dstVar]; if (!alias.indexVar) alias.indexVar = createDescriptorHeapIndexVar(dstVar); alias.imageType = found->second.imageType; @@ -5380,7 +5371,7 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( return true; } - auto &alias = descriptorHeapBufferAliasVars[dstVar]; + DescriptorHeapBufferAlias &alias = descriptorHeapBufferAliasVars[dstVar]; if (!alias.indexVar) alias.indexVar = createDescriptorHeapIndexVar(dstVar); alias.bufferPointerType = found->second.bufferPointerType; @@ -5435,11 +5426,11 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapImageTexelPointer( return ptr; } -// Descriptor-heap buffers: ConstantBuffer is a UBO (Uniform); every other -// buffer resource (Structured/RW/ByteAddress, TextureBuffer) is an SSBO -// (StorageBuffer). Here because the opaque OpTypeBufferEXT descriptor -// carries no pointee interface type, so RemoveBufferBlockVisitor -// cannot infer/correct its storage class post-lowering. +// Descriptor-heap buffers: ConstantBuffer -> Uniform (UBO); +// StructuredBuffer/RW/ByteAddress/TextureBuffer -> StorageBuffer (SSBO). +// Placed here because opaque OpTypeBufferEXT has no pointee interface type, +// so RemoveBufferBlockVisitor cannot infer/correct its storage class +// post-lowering. static spv::StorageClass getDescriptorHeapBufferStorageClass(QualType resourceType) { return isConstantBuffer(resourceType) ? spv::StorageClass::Uniform @@ -5454,12 +5445,11 @@ SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, spvBuilder); - // Select storage class and concrete layout rule for this buffer kind. + // Select storage class and layout rule for this buffer kind: // ConstantBuffer -> Uniform (UBO); all others -> StorageBuffer (SSBO). - // Passing the concrete layout rule (not Void) causes lowerType to return - // the bare struct/buffer type rather than a Uniform alias pointer, so we - // can wrap it with the correct storage class here without touching - // LowerTypeVisitor or relying on RemoveBufferBlockVisitor to fix it later. + // Passing a concrete layout rule (not Void) makes lowerType return the bare + // type rather than a Uniform alias pointer, so we apply the correct storage + // class here without patching LowerTypeVisitor or RemoveBufferBlockVisitor. const spv::StorageClass bufferSC = getDescriptorHeapBufferStorageClass(resourceType); SpirvLayoutRule layoutRule; @@ -7133,11 +7123,10 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, const Expr *indexExpr = nullptr; getDescriptorHeapOperands(expr, &baseExpr, &indexExpr); - // The heap index expression must be immediately converted to a concrete - // resource type (an implicit cast inserted by the front-end). If the - // parent is missing or is not a cast (e.g. the result is discarded as - // a statement, or used in a context with no target resource type) we - // cannot determine the resource type. + // The heap index must be immediately consumed by a front-end-inserted + // implicit cast to a concrete resource type. If the parent is missing + // or is not a cast (result discarded or no target type), the resource + // type is indeterminate. const auto *parentExpr = dyn_cast_or_null(parentMap->getParent(expr)); if (!parentExpr) { diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index f6cce7b6e4..2e4674fb4d 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -413,13 +413,12 @@ class SpirvEmitter : public ASTConsumer { /// element (descriptor) type. const SpirvType *getDescriptorHeapRuntimeArrayType(const SpirvType *elemType); - /// Emits the native (SPV_EXT_descriptor_heap) access for a buffer-like - /// resource (StructuredBuffer/ByteAddressBuffer/ConstantBuffer/TextureBuffer - /// and their RW variants) loaded from heapVar at index: - /// OpUntypedAccessChainKHR -> OpBufferPointerEXT. Records the access in - /// descriptorHeapBufferAccesses[expr] and returns the buffer-data pointer, or - /// nullptr (after emitting an error) on type-lowering failure. Caller must - /// have already checked the resource is buffer-like. + /// Emits the descriptor-heap buffer access for a buffer-like resource + /// (StructuredBuffer/ByteAddressBuffer/ConstantBuffer/TextureBuffer + RW + /// variants) loaded from heapVar at index via OpUntypedAccessChainKHR -> + /// OpBufferPointerEXT. Records in descriptorHeapBufferAccesses[expr], + /// returning the buffer-data pointer or nullptr on lowering failure. + // Caller must have checked resource is buffer-like. SpirvInstruction *emitDescriptorHeapBufferAccess( QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, const Expr *expr, const Expr *baseExpr, const Expr *indexExpr); @@ -1237,11 +1236,9 @@ class SpirvEmitter : public ASTConsumer { /// buffer alias entry for the referenced variable. bool isDescriptorHeapCounterUnsupported(const Expr *expr) const; - /// \brief Records the descriptor heap index assigned to a local image - /// resource alias, if the source expression came directly from a descriptor - /// heap. This mirrors the normal resource handle store while preserving - /// enough information to recreate OpUntypedImageTexelPointerEXT after - /// reassignment. + /// \brief Records the heap index for a local image alias when the source + /// is a descriptor heap subscript. Preserves enough information to + /// recreate OpUntypedImageTexelPointerEXT after reassignment. bool tryToAssignDescriptorHeapImageAlias(const DeclaratorDecl *dstDecl, const Expr *srcExpr); bool tryToAssignDescriptorHeapImageAlias(const Expr *dstExpr, @@ -1254,17 +1251,14 @@ class SpirvEmitter : public ASTConsumer { /// \brief Diagnoses a local resource variable assigned from both a bound /// resource and ResourceDescriptorHeap. /// - /// The alias table maps each aliased VarDecl to heap descriptor info at - /// compile time. Once a variable is recorded as an alias, every later use is - /// re-lowered as a heap access chain, ignoring control flow. That is only - /// correct when the variable holds a heap descriptor on every path reaching - /// the use, so assigning it from both kinds of source must be diagnosed - /// rather than silently miscompiled. + /// Once recorded as an alias, every use is re-lowered as a heap access + /// chain regardless of control flow, correct only if the variable holds + /// a heap descriptor on every reaching path. Mixed sources violate this; + /// reject rather than silently miscompile. /// - /// Returns true if the assignment was rejected (either a new diagnostic was - /// emitted, or the variable was already diagnosed). Callers need not act on - /// the return value: the alias-recording helpers check descriptorHeapVarState - /// themselves and skip rejected variables automatically. + /// Returns true if rejected (new diagnostic emitted or variable already + /// diagnosed); callers need not act, alias-recording helpers check + /// descriptorHeapVarState and skip rejected variables automatically. bool diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, const Expr *srcExpr, SourceLocation loc); @@ -1294,10 +1288,9 @@ class SpirvEmitter : public ASTConsumer { llvm::Optional tryToAssignToDescriptorHeapAlias(const BinaryOperator *assignExpr); - /// \brief Emits the instructions that re-derive the buffer-data pointer for a - /// descriptor-heap buffer alias decl (OpLoad of the saved index, then - /// OpUntypedAccessChainKHR + OpBufferPointerEXT). Returns nullptr if decl - /// is not a recorded heap buffer alias. Not a pure lookup -- it emits. + /// \brief Re-derives the buffer-data pointer for a heap buffer alias decl + /// (OpLoad of saved index -> OpUntypedAccessChainKHR + OpBufferPointerEXT). + /// Returns nullptr if decl is not a recorded alias. SpirvInstruction *emitDescriptorHeapBufferPointer(const VarDecl *decl, SourceLocation loc); From e182d99ac95fdf6f34858c487a63a4eff7cd4868 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 11 Sep 2026 15:06:10 -0700 Subject: [PATCH 19/45] corrected release notes --- docs/ReleaseNotes.md | 8 ++++---- tools/clang/include/clang/SPIRV/SpirvBuilder.h | 7 ++++--- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 10 +++++----- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 13 +++++-------- tools/clang/lib/SPIRV/SpirvEmitter.h | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d457ee3301..1f1e20d7bb 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -21,10 +21,10 @@ The included licenses apply to the following files: #### SPIR-V -- Added native `SPV_EXT_descriptor_heap` lowering for `ResourceDescriptorHeap` - and `SamplerDescriptorHeap` via `-fspv-use-descriptor-heap`. Requires - `-fspv-target-env=vulkan1.3` - [#8517](https://github.com/microsoft/DirectXShaderCompiler/pull/8517). +- Extended `SPV_EXT_descriptor_heap` lowering (`-fspv-use-descriptor-heap`) to + buffer-like resources (`StructuredBuffer`, `ByteAddressBuffer`, + `ConstantBuffer`, `TextureBuffer`, and RW variants) via + `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3`. Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 6a2f3751fb..afd8f42849 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -831,9 +831,10 @@ class SpirvBuilder { bool specConst = false); SpirvConstant *getConstantNull(QualType); SpirvConstant *getConstantString(llvm::StringRef str, bool specConst = false); - /// \brief Returns the OpConstantSizeOfEXT (SPV_EXT_descriptor_heap) for descriptor operandType: - /// client-API size in bytes as a 32-bit unsigned integer. Cached per operand - /// type. + + /// \brief Returns the OpConstantSizeOfEXT (SPV_EXT_descriptor_heap) for + /// descriptor operandType: client-API size in bytes as a 32-bit unsigned + /// integer. Cached per operand type. SpirvConstant *getConstantSizeOfEXT(const SpirvType *operandType); SpirvSpecConstantTernaryOp * diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index 4ccb03544d..e8fa6c097e 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2057,11 +2057,11 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { // max(sizeof(image), sizeof(buffer)). // VkPhysicalDeviceDescriptorHeapPropertiesEXT reports one size per category // (imageDescriptorSize / bufferDescriptorSize); textures lower to - // OpTypeImage, so image/buffer covers all relevant HLSL resource kinds. Sizes are - // driver-defined (known only at pipeline creation), so the max is computed - // via OpSpecConstantOp over two OpConstantSizeOfEXT placeholders. A canonical - // sampled 2D float image and a Uniform buffer serve as representatives; - // subtype and storage class do not affect the category size. + // OpTypeImage, so image/buffer covers all relevant HLSL resource kinds. Sizes + // are driver-defined (known only at pipeline creation), so the max is + // computed via OpSpecConstantOp over two OpConstantSizeOfEXT placeholders. + // A canonical sampled 2D float image and a Uniform buffer serve as + // representatives; subtype and storage class do not affect the category size. const SpirvType *placeholderImage = context.getImageType( context.getFloatType(32), spv::Dim::Dim2D, ImageType::WithDepth::No, /*arrayed*/ false, /*ms*/ false, ImageType::WithSampler::Yes, diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 99758b1854..02746eaa50 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5164,10 +5164,9 @@ SpirvEmitter::processStructuredBufferLoad(const CXXMemberCallExpr *expr) { // derefOrCreatePointerToValue returns an lvalue (AccessChain) when the base // is an lvalue (SPIR-V pointer into the buffer element) not the value. // StructuredBuffer::Load must return the element value, so load explicitly. - if (result && !result->isRValue()) { + if (result && !result->isRValue()) result = spvBuilder.createLoad(expr->getType(), result, buffer->getExprLoc(), range); - } return result; } @@ -5334,12 +5333,10 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias( isAKindOfStructuredOrByteBuffer(dstVar->getType()))) return false; - { - auto stateIt = descriptorHeapVarState.find(dstVar); - if (stateIt != descriptorHeapVarState.end() && - stateIt->second == DescriptorHeapVarState::Mixed) - return false; - } + auto stateIt = descriptorHeapVarState.find(dstVar); + if (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Mixed) + return false; const Expr *src = srcExpr->IgnoreParenCasts(); auto found = descriptorHeapBufferAccesses.find(src); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 2e4674fb4d..8a3460ae8e 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -417,7 +417,7 @@ class SpirvEmitter : public ASTConsumer { /// (StructuredBuffer/ByteAddressBuffer/ConstantBuffer/TextureBuffer + RW /// variants) loaded from heapVar at index via OpUntypedAccessChainKHR -> /// OpBufferPointerEXT. Records in descriptorHeapBufferAccesses[expr], - /// returning the buffer-data pointer or nullptr on lowering failure. + /// returning the buffer-data pointer or nullptr on lowering failure. // Caller must have checked resource is buffer-like. SpirvInstruction *emitDescriptorHeapBufferAccess( QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, From 70c4bb9c4ef3886dacd8acefbd5e4997cf32f9a2 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 16 Sep 2026 17:20:43 -0700 Subject: [PATCH 20/45] appease copilot --- docs/SPIR-V.rst | 4 ++++ tools/clang/lib/SPIRV/SpirvEmitter.cpp | 2 +- .../CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl | 7 +++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/SPIR-V.rst b/docs/SPIR-V.rst index 9d04b7e6f8..18b6609d61 100644 --- a/docs/SPIR-V.rst +++ b/docs/SPIR-V.rst @@ -2178,6 +2178,10 @@ loop:: Supporting these forms requires modelling the alias as a value with real control-flow merges instead of as compile-time state. +``StructuredBuffer``, ``ByteAddressBuffer``, and related buffer-type aliases +cannot yet be passed to or returned from user functions; access the resource +directly at each call or return site. + Two further restrictions on the heap access expression itself produce diagnostics. The object being subscripted must be a direct reference to the builtin ``ResourceDescriptorHeap`` or ``SamplerDescriptorHeap`` variable, and diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 02746eaa50..6dc6d9d776 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -1228,7 +1228,7 @@ SpirvInstruction *SpirvEmitter::doExpr(const Expr *expr, if (isImplicitVarDeclInVkNamespace(declRefExpr->getDecl())) { result = doExpr(cast(decl)->getInit()); } else if (const auto *varDecl = dyn_cast(decl)) { - if (auto *alias = + if (SpirvInstruction *alias = emitDescriptorHeapBufferPointer(varDecl, expr->getLocStart())) result = alias; else diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl index 591fac84ff..469fd4f845 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.nonuniform.hlsl @@ -5,14 +5,17 @@ // SPV_EXT_descriptor_heap deprecates the NonUniform decoration; drivers handle // divergent indices natively and the decoration must be omitted. +// The two NonUniformResourceIndex results retain NonUniform; it must not +// propagate to access chains or loaded handles. +// CHECK: OpDecorate %{{.*}} NonUniform +// CHECK: OpDecorate %{{.*}} NonUniform +// CHECK-NOT: OpDecorate %{{.*}} NonUniform // CHECK-DAG: %[[UntypedPtrType:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant // CHECK-DAG: %[[Tex2DType:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown // CHECK-DAG: %[[SamplerType:[a-zA-Z0-9_]+]] = OpTypeSampler // CHECK-DAG: %[[RA_Tex2DType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Tex2DType]] // CHECK-DAG: %[[RA_SamplerType:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[SamplerType]] -// CHECK-NOT: OpDecorate %{{.*}} NonUniform - // CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant // CHECK: %[[SamplerHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedPtrType]] UniformConstant From bcae914b822905664d5d17821196ab5c45ffb153 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 11 Sep 2026 15:06:10 -0700 Subject: [PATCH 21/45] corrected release notes --- docs/ReleaseNotes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1f1e20d7bb..cc65e3752a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -24,7 +24,8 @@ The included licenses apply to the following files: - Extended `SPV_EXT_descriptor_heap` lowering (`-fspv-use-descriptor-heap`) to buffer-like resources (`StructuredBuffer`, `ByteAddressBuffer`, `ConstantBuffer`, `TextureBuffer`, and RW variants) via - `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3`. + `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3` + [#8517](https://github.com/microsoft/DirectXShaderCompiler/pull/8517). Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. From 6169170e77f7d898760bda29d6b4f43b6b32c7d5 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 4 Jun 2026 13:19:50 -0700 Subject: [PATCH 22/45] [SPIR-V] Add descriptor heap RaytracingAccelerationStructure support Extends the SPV_EXT_descriptor_heap native heap lowering to cover RaytracingAccelerationStructure resources loaded from ResourceDescriptorHeap. Acceleration structure descriptors are accessed via OpUntypedAccessChainKHR into a runtime array of OpTypeAccelerationStructureKHR, consistent with the image and sampler paths added in the previous commit. --- .../clang/include/clang/SPIRV/AstTypeProbe.h | 4 ++ tools/clang/lib/SPIRV/AstTypeProbe.cpp | 7 ++++ tools/clang/lib/SPIRV/SpirvEmitter.cpp | 10 +++++ ...riptorheap.ext.acceleration-structure.hlsl | 42 +++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl diff --git a/tools/clang/include/clang/SPIRV/AstTypeProbe.h b/tools/clang/include/clang/SPIRV/AstTypeProbe.h index 214df82d45..207d02a3b2 100644 --- a/tools/clang/include/clang/SPIRV/AstTypeProbe.h +++ b/tools/clang/include/clang/SPIRV/AstTypeProbe.h @@ -250,6 +250,10 @@ bool isBuffer(QualType type); /// \brief Returns true if the given type is the HLSL RWBuffer type. bool isRWBuffer(QualType type); +/// \brief Returns true if the given type is the HLSL +/// RaytracingAccelerationStructure type. +bool isRaytracingAccelerationStructure(QualType type); + /// \brief Returns true if the given type is an HLSL Texture type. bool isTexture(QualType); diff --git a/tools/clang/lib/SPIRV/AstTypeProbe.cpp b/tools/clang/lib/SPIRV/AstTypeProbe.cpp index a871bd3577..8c766445a3 100644 --- a/tools/clang/lib/SPIRV/AstTypeProbe.cpp +++ b/tools/clang/lib/SPIRV/AstTypeProbe.cpp @@ -917,6 +917,13 @@ bool isBuffer(QualType type) { return false; } +bool isRaytracingAccelerationStructure(QualType type) { + if (const auto *rt = type->getAs()) { + return rt->getDecl()->getName() == "RaytracingAccelerationStructure"; + } + return false; +} + bool isRWTexture(QualType type) { if (const auto *rt = type->getAs()) { const auto name = rt->getDecl()->getName(); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 6dc6d9d776..41cb91bcfa 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2034,6 +2034,16 @@ bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, return tryToAssignDescriptorHeapBufferAlias(decl, init); } + if (isRaytracingAccelerationStructure(decl->getType())) { + if (auto *initVal = loadIfGLValue(init)) + declIdMapper.registerFnVarAlias(decl, initVal); + else + emitError("cannot create descriptor heap acceleration structure alias " + "from initializer", + init->getExprLoc()); + return true; + } + return false; } diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl new file mode 100644 index 0000000000..7c5c9fdcb9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl @@ -0,0 +1,42 @@ +// RUN: %dxc -T lib_6_6 -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -fvk-resource-heap-stride 64 -fvk-sampler-heap-stride 32 -fspv-extension=SPV_KHR_ray_tracing -fspv-extension=SPV_EXT_descriptor_heap -fspv-extension=SPV_KHR_untyped_pointers -spirv %s | FileCheck %s + +// Verifies: ResourceDescriptorHeap of a RaytracingAccelerationStructure +// lowers to an acceleration-structure runtime array, loads the accel +// handle from the heap, and emits OpTraceRayKHR under the ray-tracing +// capability/extension. + +// CHECK: OpCapability RayTracingKHR +// CHECK: OpExtension "SPV_KHR_ray_tracing" + +// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR +// CHECK-DAG: %[[ASArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant + +struct Payload { + float4 color; +}; + +struct Attribute { + float2 bary; +}; + +[shader("closesthit")] +void main(inout Payload payload, in Attribute attr) { + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[3]; + + RayDesc ray; + ray.Origin = float3(0.0f, 0.0f, 0.0f); + ray.Direction = float3(0.0f, 0.0f, -1.0f); + ray.TMin = 0.0f; + ray.TMax = 1000.0f; + + Payload childPayload = { float4(attr.bary, 0.0f, 1.0f) }; + + // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[ASArray]] %[[ResourceHeap]] %uint_3 + // CHECK: %[[Scene:[a-zA-Z0-9_]+]] = OpLoad %[[Accel]] %[[Desc]] + // CHECK: OpTraceRayKHR %[[Scene]] + TraceRay(scene, 0x0, 0xff, 0, 1, 0, ray, childPayload); + + payload.color = childPayload.color; +} From 75c5684c4d6982d02970d895a3e38cb68209f782 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 20 Jul 2026 15:40:40 -0700 Subject: [PATCH 23/45] removed erronous CLI options from test --- .../sm6_6.descriptorheap.ext.acceleration-structure.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl index 7c5c9fdcb9..57ba2c3d68 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl @@ -1,4 +1,4 @@ -// RUN: %dxc -T lib_6_6 -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -fvk-resource-heap-stride 64 -fvk-sampler-heap-stride 32 -fspv-extension=SPV_KHR_ray_tracing -fspv-extension=SPV_EXT_descriptor_heap -fspv-extension=SPV_KHR_untyped_pointers -spirv %s | FileCheck %s +// RUN: %dxc -T lib_6_6 -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -fspv-extension=SPV_KHR_ray_tracing -fspv-extension=SPV_EXT_descriptor_heap -fspv-extension=SPV_KHR_untyped_pointers -spirv %s | FileCheck %s // Verifies: ResourceDescriptorHeap of a RaytracingAccelerationStructure // lowers to an acceleration-structure runtime array, loads the accel From 862b41aa618c908bd7500c2b2e0170a0b4de7d70 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 22 Jul 2026 14:12:52 -0700 Subject: [PATCH 24/45] Included AccelerationStructure in resource heap stride calculation --- .../clang/include/clang/SPIRV/SpirvBuilder.h | 30 +++- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 41 +++-- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 103 +++++++++++- ...t.acceleration-structure.stride.error.hlsl | 46 ++++++ ...ptorheap.ext.array-stride.accelstruct.hlsl | 154 ++++++++++++++++++ 5 files changed, 354 insertions(+), 20 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index afd8f42849..521de17277 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -842,9 +842,27 @@ class SpirvBuilder { SpirvInstruction *op1, SpirvInstruction *op2, SpirvInstruction *op3, SourceLocation loc); - /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays: - /// max(sizeof(image), sizeof(buffer)) computed via OpSpecConstantOp. - /// Cached per module. + /// \brief Record that acceleration structures may occupy the resource heap. + /// Note: Must be called before getResourceHeapArrayStride() (before the + /// code-gen loop in HandleTranslationUnit) so the cached stride is correct + /// on the first call. Calling it later has no effect because the result is + /// frozen after the first getResourceHeapArrayStride() invocation. + void noteResourceHeapHasAccelStruct() { resourceHeapHasAccelStruct = true; } + + /// \brief Returns whether the resource-heap stride accounts for acceleration + /// structure descriptors. Code-gen must reject an acceleration structure heap + /// access when this is false: the decision is made before the code-gen loop + /// and the stride cannot be widened afterwards. + bool resourceHeapStrideIncludesAccelStruct() const { + return resourceHeapHasAccelStruct; + } + + /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays. + /// Default: max(sizeof(image), sizeof(buffer)) + /// With RT: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) + /// Computed via OpSpecConstantOp and cached per module. + /// Note: noteResourceHeapHasAccelStruct() must be called before this if AS + /// may be present (result is frozen on the first call). SpirvInstruction *getResourceHeapArrayStride(); /// \brief Shared ArrayStrideIdEXT operand for sampler-heap runtime arrays: @@ -975,6 +993,12 @@ class SpirvBuilder { SpirvInstruction *resourceHeapArrayStride = nullptr; SpirvInstruction *samplerHeapArrayStride = nullptr; + /// Set by noteResourceHeapHasAccelStruct() when HandleTranslationUnit + /// detects that the shader uses ray-tracing features. When true, + /// getResourceHeapArrayStride() extends the stride to include + /// sizeof(acceleration_structure). + bool resourceHeapHasAccelStruct = false; + SpirvDebugInfoNone *debugNone; /// DebugExpression that does not reference any DebugOperation diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index e8fa6c097e..a447092828 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2052,16 +2052,24 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { if (resourceHeapArrayStride) return resourceHeapArrayStride; - // ResourceDescriptorHeap is a flat array: any descriptor may sit at any slot. - // DX12 semantics require all resource arrays share stride = - // max(sizeof(image), sizeof(buffer)). + // ResourceDescriptorHeap is a flat array; any descriptor may sit at any slot. + // DX12 semantics require all resource arrays share one stride: + // stride = max(sizeof(image), sizeof(buffer) [, sizeof(accel_struct)]) + // + // Categories and placeholders: + // image: - always included; textures lower to OpTypeImage, so this covers + // all non-RT resource kinds. Representative: canonical sampled + // 2D float image (subtype/storage class do not affect size). + // buffer - always included. Representative: Uniform buffer. + // accel - included only when noteResourceHeapHasAccelStruct() was called. + // Placeholder: getAccelerationStructureType(). Requires + // OpCapability RayTracingKHR, guaranteed present when + // noteResourceHeapHasAccelStruct() is called. + // // VkPhysicalDeviceDescriptorHeapPropertiesEXT reports one size per category - // (imageDescriptorSize / bufferDescriptorSize); textures lower to - // OpTypeImage, so image/buffer covers all relevant HLSL resource kinds. Sizes - // are driver-defined (known only at pipeline creation), so the max is - // computed via OpSpecConstantOp over two OpConstantSizeOfEXT placeholders. - // A canonical sampled 2D float image and a Uniform buffer serve as - // representatives; subtype and storage class do not affect the category size. + // (imageDescriptorSize / bufferDescriptorSize); all sizes are driver-defined. + // The max is computed via OpSpecConstantOp over OpConstantSizeOfEXT + // placeholders: 2 for non-RT shaders, 3 when accel struct is present. const SpirvType *placeholderImage = context.getImageType( context.getFloatType(32), spv::Dim::Dim2D, ImageType::WithDepth::No, /*arrayed*/ false, /*ms*/ false, ImageType::WithSampler::Yes, @@ -2073,9 +2081,22 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { SpirvInstruction *bufferSize = getConstantSizeOfEXT(placeholderBuffer); SpirvInstruction *imageIsBigger = createSpecConstantBinaryOp( spv::Op::OpUGreaterThan, astContext.BoolTy, imageSize, bufferSize, {}); - resourceHeapArrayStride = + SpirvInstruction *maxImgBuf = createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, imageIsBigger, imageSize, bufferSize, {}); + + if (resourceHeapHasAccelStruct) { + // Extend to max(max(img, buf), accel_struct). + const SpirvType *placeholderAS = context.getAccelerationStructureTypeNV(); + SpirvInstruction *asSize = getConstantSizeOfEXT(placeholderAS); + SpirvInstruction *maxImgBufIsBigger = createSpecConstantBinaryOp( + spv::Op::OpUGreaterThan, astContext.BoolTy, maxImgBuf, asSize, {}); + resourceHeapArrayStride = + createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, + maxImgBufIsBigger, maxImgBuf, asSize, {}); + } else { + resourceHeapArrayStride = maxImgBuf; + } return resourceHeapArrayStride; } diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 41cb91bcfa..9394ef5e02 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -67,6 +67,23 @@ bool isImplicitVarDeclInVkNamespace(const Decl *decl) { return false; } +// Returns true if the given shader model kind is a ray-tracing stage. +// Used to detect whether the resource heap stride must include +// sizeof(acceleration_structure). +bool shaderModelKindIsRayTracing(hlsl::ShaderModel::Kind k) { + switch (k) { + case hlsl::ShaderModel::Kind::RayGeneration: + case hlsl::ShaderModel::Kind::Intersection: + case hlsl::ShaderModel::Kind::AnyHit: + case hlsl::ShaderModel::Kind::ClosestHit: + case hlsl::ShaderModel::Kind::Miss: + case hlsl::ShaderModel::Kind::Callable: + return true; + default: + return false; + } +} + // Returns true if the given decl has the given semantic. bool hasSemantic(const DeclaratorDecl *decl, hlsl::DXIL::SemanticKind semanticKind) { @@ -803,6 +820,57 @@ void SpirvEmitter::HandleTranslationUnit(ASTContext &context) { return; } + // Pre-detect whether the resource-heap array stride must include the + // acceleration structure descriptor size. + // + // getResourceHeapArrayStride() caches its result on the first call. All + // resource runtime arrays created during code-gen share that one cached + // stride instruction pointer. To make the value correct the decision must + // be made here, before any descriptor-heap subscript expression is evaluated. + // + // Specifically, needsAccelStruct is set true under any of three conditions: + // + // Condition 1, ray-tracing entry point: any workQueue entry is an RT stage. + // RT stages unconditionally emit OpCapability RayTracingKHR; that capability + // is what permits OpConstantSizeOfEXT on AccelerationStructureKHR, so the + // instruction is safe to emit. + // + // Condition 2, explicit KHR_ray_tracing / NV_ray_tracing extension: a + // compute or graphics shader declared one of these extensions explicitly, + // meaning it may use acceleration structures via descriptor heap. + // + // Condition 3, explicit KHR_ray_query extension: same rationale for + // RayQuery users. + // + // isExtensionEnabled() cannot be used without the guard: in default mode + // (no -fspv-extension flags), FeatureManager enables all by-default + // extensions (including KHR_ray_tracing and KHR_ray_query) causing false + // positives. The guard !spirvOptions.allowedExtensions.empty() reliably + // distinguishes "user-listed" from "allowed by default." + // + // Note: the global-decl pass above runs before this block. HLSL forbids + // descriptor-heap access in global initializers, so + // getResourceHeapArrayStride() cannot be called there. If that restriction is + // ever lifted, this block should be moved before that pass. + if (spirvOptions.useDescriptorHeap) { + bool needsAccelStruct = false; + + for (const FunctionInfo *fi : workQueue) + if (shaderModelKindIsRayTracing(fi->shaderModelKind)) { + needsAccelStruct = true; + break; + } + + if (!needsAccelStruct && !spirvOptions.allowedExtensions.empty()) + needsAccelStruct = + featureManager.isExtensionEnabled(Extension::KHR_ray_tracing) || + featureManager.isExtensionEnabled(Extension::NV_ray_tracing) || + featureManager.isExtensionEnabled(Extension::KHR_ray_query); + + if (needsAccelStruct) + spvBuilder.noteResourceHeapHasAccelStruct(); + } + // Translate all functions reachable from the entry function. // The queue can grow in the meanwhile; so need to keep evaluating // workQueue.size(). @@ -7165,6 +7233,21 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, return nullptr; } + // The resource-heap stride is decided before the code-gen loop and is + // frozen on its first use, so it cannot be widened here. Reject rather + // than emit a stride that may be too narrow for the acceleration + // structure descriptor. + if (isRaytracingAccelerationStructure(resourceType) && + !spvBuilder.resourceHeapStrideIncludesAccelStruct()) { + emitError("acceleration structure loaded from ResourceDescriptorHeap " + "requires the resource heap stride to account for " + "acceleration structure descriptors; compile with " + "-fspv-extension=SPV_KHR_ray_tracing or " + "-fspv-extension=SPV_KHR_ray_query", + expr->getExprLoc()); + return nullptr; + } + if (isAKindOfStructuredOrByteBuffer(resourceType) || isConstantTextureBuffer(resourceType)) return emitDescriptorHeapBufferAccess(resourceType, var, index, expr, @@ -9397,13 +9480,19 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { const SpirvType * SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType) { - // SPV_EXT_descriptor_heap: apply a client-API-defined byte stride via an - // ArrayStrideIdEXT decoration. The sampler heap holds a single descriptor - // type, so its stride is the sampler descriptor size. The resource heap is a - // shared flat array in which any resource descriptor may sit at any slot, so - // every resource runtime array must use one common stride: max(sizeof(image), - // sizeof(buffer)). Using the accessed element size would be wrong for the - // resource heap. + // Apply a client-API-defined byte stride via an ArrayStrideIdEXT decoration. + // The sampler heap holds a single descriptor type, so its stride is the + // sampler descriptor size. The resource heap is a shared flat array in which + // any resource descriptor may sit at any slot, so every resource runtime + // array must use one common stride. + // + // [non-RT shaders]: + // max(sizeof(image), sizeof(buffer)) + // [RT shaders]: + // max(sizeof(image), sizeof(buffer), sizeof(acceleration_structure)) + // + // The stride is determined once before the code-gen loop and cached in + // spvBuilder. SpirvInstruction *strideId = isa(elemType) ? spvBuilder.getSamplerHeapArrayStride() : spvBuilder.getResourceHeapArrayStride(); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl new file mode 100644 index 0000000000..5c94f614b9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl @@ -0,0 +1,46 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_query \ +// RUN: -spirv %s | FileCheck %s --check-prefix=OK + +// Verifies: an acceleration structure loaded from ResourceDescriptorHeap is +// rejected when the resource-heap stride was not widened to include the +// acceleration structure descriptor size. +// +// The stride is decided before the code-gen loop (see HandleTranslationUnit) +// and frozen on its first use, so it cannot be widened once an acceleration +// structure heap access is reached. Without the widening this shader would +// silently get max(sizeof(image), sizeof(buffer)), which may be too narrow. +// +// A compute shader is not a ray-tracing stage, so the widening only happens +// when the user lists a ray-tracing/ray-query extension explicitly. The first +// run does not, and must fail; the second one does, and must compile. + +// CHECK: error: acceleration structure loaded from ResourceDescriptorHeap requires the resource heap stride to account for acceleration structure descriptors; compile with -fspv-extension=SPV_KHR_ray_tracing or -fspv-extension=SPV_KHR_ray_query + +// OK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR +// OK-DAG: %[[AccelSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] + +RWBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; + + RayDesc ray; + ray.Origin = float3(0.0, 0.0, 0.0); + ray.Direction = float3(0.0, 0.0, 1.0); + ray.TMin = 0.0; + ray.TMax = 1000.0; + + RayQuery q; + q.TraceRayInline(scene, RAY_FLAG_NONE, 0xff, ray); + bool hit = q.Proceed(); + + output[tid.x] = float4(hit ? 1.0 : 0.0, 0.0, 0.0, 0.0); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl new file mode 100644 index 0000000000..aabb9aa5b9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -0,0 +1,154 @@ +// Two test paths share this file; select via -D RT_STAGE: +// +// Path A; Condition 1 (RT stage): +// -T lib_6_6 -D RT_STAGE -fspv-extension=SPV_KHR_ray_tracing +// A closesthit entry point is in the workQueue; shaderModelKindIsRayTracing() +// returns true and noteResourceHeapHasAccelStruct() is called unconditionally. +// +// Path B; Condition 3 (explicit KHR_ray_query extension): +// -T cs_6_6 -E main -fspv-extension=SPV_KHR_ray_query +// No RT stage in workQueue, so Condition 1 is skipped. The +// !spirvOptions.allowedExtensions.empty() guard passes (user listed an +// explicit extension), isExtensionEnabled(KHR_ray_query) is true, and +// noteResourceHeapHasAccelStruct() is called. +// +// Both paths verify that the shared resource-heap array stride expands to +// max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) +// and that ALL resource runtime arrays share that three-way max stride. +// +// Ordering stress test: Texture2D is accessed BEFORE the AS in source order. +// The stride cache must already hold sizeof(accel_struct) when the first +// runtime array type is created so the texture array uses the correct stride. +// +// The regression test for the "default-extension-mode" false positive (no +// -fspv-extension flags -> allowedExtensions is empty -> Condition 3 is skipped) +// is sm6_6.descriptorheap.ext.array-stride.hlsl, which must emit exactly 3 +// OpConstantSizeOfEXT (img, buf, sampler - no accel_struct). + +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_tracing \ +// RUN: -spirv %s | FileCheck %s --check-prefixes=CHECK,RT + +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_query \ +// RUN: -spirv %s | FileCheck %s + +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_tracing \ +// RUN: -spirv %s | FileCheck %s --check-prefix=SZRT + +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_query \ +// RUN: -spirv %s | FileCheck %s --check-prefix=SZRQ + +// --- Types (user-declared + stride-computation placeholders) --------------- +// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR +// CHECK-DAG: %[[Img:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown + +// UBuf is not user-declared; it is the canonical placeholder injected by +// getResourceHeapArrayStride() for the buffer descriptor size. +// CHECK-DAG: %[[UBuf:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform + +// Sampler only present in path A (closesthit). +// RT-DAG: %[[Samp:[a-zA-Z0-9_]+]] = OpTypeSampler + +// --- Runtime array types --------------------------------------------------- +// CHECK-DAG: %[[AccelArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] +// CHECK-DAG: %[[ImgArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Img]] + +// RT-DAG: %[[SampArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Samp]] + +// --- Size operands --------------------------------------------------------- +// CHECK-DAG: %[[ImgSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Img]] +// CHECK-DAG: %[[BufSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBuf]] +// CHECK-DAG: %[[ASSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] +// RT-DAG: %[[SampSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Samp]] + +// --- Three-way max: max(max(img, buf), accel) identical in both paths ---- +// CHECK-DAG: %[[IB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[MaxIB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IB]] %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[IBA:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[MaxIB]] %[[ASSz]] +// CHECK-DAG: %[[Stride:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IBA]] %[[MaxIB]] %[[ASSz]] + +// --- All resource arrays share the three-way-max stride ------------------- +// %[[ImgArr]] is created FIRST (texture before AS in source); most important. +// If the stride cache were populated before noteResourceHeapHasAccelStruct() +// was called, ImgArr would be decorated with a two-way max stride, which +// would not match %[[Stride]] (bound to the three-way max above), failing here. +// CHECK-DAG: OpDecorateId %[[ImgArr]] ArrayStrideIdEXT %[[Stride]] +// CHECK-DAG: OpDecorateId %[[AccelArr]] ArrayStrideIdEXT %[[Stride]] + +// --- Sampler stride is independent (path A only) -------------------------- +// RT-DAG: OpDecorateId %[[SampArr]] ArrayStrideIdEXT %[[SampSz]] + +// --- Exact OpConstantSizeOfEXT counts ------------------------------------- +// Path A: img, buf, accel, sampler = 4 +// SZRT-COUNT-4: OpConstantSizeOfEXT %uint +// SZRT-NOT: OpConstantSizeOfEXT %uint + +// Path B: img, buf, accel = 3 (no sampler) +// SZRQ-COUNT-3: OpConstantSizeOfEXT %uint +// SZRQ-NOT: OpConstantSizeOfEXT %uint + +#ifdef RT_STAGE + +struct Payload { float4 color; }; +struct Attribute { float2 bary; }; + +[shader("closesthit")] +void main(inout Payload payload, in Attribute attr) { + // Ordering stress test: access texture first + Texture2D tex = ResourceDescriptorHeap[0]; + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; + SamplerState samp = SamplerDescriptorHeap[0]; + + float4 color = tex.SampleLevel(samp, float2(0.0, 0.0), 0.0); + payload.color = color; + + RayDesc ray; + ray.Origin = float3(0.0, 0.0, 0.0); + ray.Direction = float3(0.0, 0.0, -1.0); + ray.TMin = 0.0; + ray.TMax = 1000.0; + + Payload child = { color }; + TraceRay(scene, 0x0, 0xff, 0, 1, 0, ray, child); +} + +#else // !RT_STAGE, compute shader with RayQuery (path B) + +RWBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + // Ordering stress test: access texture first + Texture2D tex = ResourceDescriptorHeap[0]; + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; + + RayDesc ray; + ray.Origin = float3(0.0, 0.0, 0.0); + ray.Direction = float3(0.0, 0.0, 1.0); + ray.TMin = 0.0; + ray.TMax = 1000.0; + + RayQuery q; + q.TraceRayInline(scene, RAY_FLAG_NONE, 0xff, ray); + bool hit = q.Proceed(); + + int3 coord = int3(tid.x, 0, 0); + output[tid.x] = tex.Load(coord) + float4(hit ? 1.0 : 0.0, 0.0, 0.0, 0.0); +} + +#endif // RT_STAGE \ No newline at end of file From 56c6ea405873c8d84969ed63992f57aa31f55d96 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 3 Aug 2026 10:03:44 -0700 Subject: [PATCH 25/45] Added AS reassignment diagnostic, fixed tests and docs --- docs/SPIR-V.rst | 61 ++++++-- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 14 ++ tools/clang/lib/SPIRV/DeclResultIdMapper.h | 7 + tools/clang/lib/SPIRV/SpirvEmitter.cpp | 43 ++++-- tools/clang/lib/SPIRV/SpirvEmitter.h | 5 +- ...t.acceleration-structure.stride.error.hlsl | 2 +- ...ptorheap.ext.array-stride.accelstruct.hlsl | 131 ++++++++++-------- 7 files changed, 178 insertions(+), 85 deletions(-) diff --git a/docs/SPIR-V.rst b/docs/SPIR-V.rst index 18b6609d61..125c185702 100644 --- a/docs/SPIR-V.rst +++ b/docs/SPIR-V.rst @@ -2143,13 +2143,27 @@ handle into a function-scope image variable: %texel_ptr = OpUntypedImageTexelPointerEXT %uptr_image %image_type %descriptor %coord %sample %old = OpAtomicIAdd %uint %texel_ptr %scope %semantics %value +``RaytracingAccelerationStructure`` resources loaded from +``ResourceDescriptorHeap`` follow the same access-chain-then-load shape over a +runtime array of ``OpTypeAccelerationStructureKHR``: + +.. code:: spirv + + %accel_type = OpTypeAccelerationStructureKHR + %accel_array = OpTypeRuntimeArray %accel_type + OpDecorateId %accel_array ArrayStrideIdEXT %resource_stride + %descriptor = OpUntypedAccessChainKHR %uptr_uc %accel_array %resource_heap %index + %accel = OpLoad %accel_type %descriptor + This path supports texture, RWTexture, sampler, Buffer/RWBuffer, StructuredBuffer/RWStructuredBuffer without associated counter operations, -ByteAddressBuffer/RWByteAddressBuffer, ConstantBuffer, and TextureBuffer heap -loads, including direct field and array-element accesses for -``ConstantBuffer`` and ``TextureBuffer``. ``NonUniformResourceIndex`` is -accepted, but no ``NonUniform`` decoration is emitted on the -``OpUntypedAccessChainKHR`` result or on the loaded descriptor; +ByteAddressBuffer/RWByteAddressBuffer, ConstantBuffer, TextureBuffer, and +``RaytracingAccelerationStructure`` heap loads, including direct field and +array-element accesses for ``ConstantBuffer`` and ``TextureBuffer``. +Acceleration structure loads are additionally subject to the stride requirement +described in `Descriptor heap array stride`_ below. +``NonUniformResourceIndex`` is accepted, but no ``NonUniform`` decoration is +emitted on the ``OpUntypedAccessChainKHR`` result or on the loaded descriptor; ``SPV_EXT_descriptor_heap`` deprecates the decoration for heap accesses and drivers handle divergent heap indices natively. The index operand itself may still carry ``NonUniform`` from the surrounding expression. @@ -2184,11 +2198,13 @@ directly at each call or return site. Two further restrictions on the heap access expression itself produce diagnostics. The object being subscripted must be a direct reference to the -builtin ``ResourceDescriptorHeap`` or ``SamplerDescriptorHeap`` variable, and -the subscript result must be immediately converted to a concrete resource type -so that DXC can select a descriptor type for the access. A subscript whose -result is discarded, or used in a context that supplies no target resource -type, is rejected. +builtin ``ResourceDescriptorHeap`` or ``SamplerDescriptorHeap`` variable; the +subscript result must be immediately converted to a concrete resource type so +that DXC can select a descriptor type for the access, so a subscript whose +result is discarded or used in a context that supplies no target resource type +is rejected; and a local ``RaytracingAccelerationStructure`` must be initialized +from a loadable heap access, since the alias has no backing descriptor +otherwise. Descriptor heap array stride ++++++++++++++++++++++++++++ @@ -2198,7 +2214,8 @@ rather than a literal ``ArrayStride``, because descriptor sizes are not known until pipeline creation. The shared value is built from ``OpConstantSizeOfEXT`` and ``OpSpecConstantOp`` and evaluates to ``max(sizeof(image_descriptor), sizeof(buffer_descriptor))``. The sampler heap -carries its own ``ArrayStrideIdEXT`` equal to ``sizeof(sampler_descriptor)``. +carries its own ``ArrayStrideIdEXT`` equal to ``sizeof(sampler_descriptor)``, +regardless of resource heap contents. The ``OpConstantSizeOfEXT`` operands are placeholder types chosen only for their descriptor class. All image types report the same descriptor size, so the @@ -2207,6 +2224,28 @@ types the shader actually uses; a module will normally contain both the placeholder type and the distinct image types its heap accesses lower to. The stride value is built once and cached on first use. +When acceleration structure descriptors may appear on the resource heap, the +formula expands to a three-way max +``max(max(sizeof(image_descriptor), sizeof(buffer_descriptor)), sizeof(acceleration_structure))``. + +Because the stride is cached on first use, this decision is committed before +code generation and is **not** based on whether the shader actually performs an +acceleration structure heap load: a ray-tracing shader that only heap-loads a +texture still gets the three-way max. The widening happens when either + +- any entry point is a ray-tracing stage, or +- the user explicitly passed ``-fspv-extension=SPV_KHR_ray_tracing``, + ``-fspv-extension=SPV_NV_ray_tracing``, or + ``-fspv-extension=SPV_KHR_ray_query``. + +The second condition requires an explicit ``-fspv-extension`` flag. In the +default extension mode DXC allows the ray-tracing and ray-query extensions +implicitly, but that does not widen the stride. Because the stride cannot be +widened once it has been built, a shader that is not a ray-tracing stage and +loads a ``RaytracingAccelerationStructure`` from ``ResourceDescriptorHeap`` +without an explicit ray extension flag is rejected rather than given a stride +that may be too narrow. + HLSL Expressions ================ diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 3a88334ffc..4660e8a9e0 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -1136,6 +1136,20 @@ DeclResultIdMapper::createFnVar(const VarDecl *var, return varInstr; } +void DeclResultIdMapper::registerFnVarAlias(const VarDecl *var, + SpirvInstruction *varInstr) { + if (varInstr) + registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); +} + +bool DeclResultIdMapper::hasFnVarAlias(const VarDecl *var) const { + const DeclSpirvInfo *info = getDeclSpirvInfo(var); + if (!info || !info->instr) + return false; + return !isa(info->instr); +} + + SpirvDebugGlobalVariable *DeclResultIdMapper::createDebugGlobalVariable( SpirvVariable *var, const QualType &type, const SourceLocation &loc, const StringRef &name) { diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.h b/tools/clang/lib/SPIRV/DeclResultIdMapper.h index 6a96ae8f45..9fa590efbf 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.h +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.h @@ -285,6 +285,13 @@ class DeclResultIdMapper { SpirvVariable *createFnVar(const VarDecl *var, llvm::Optional init); + /// \brief Registers a function-scope alias to an existing instruction. + void registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr); + + /// \brief Returns true if the decl was registered via registerFnVarAlias, + bool hasFnVarAlias(const VarDecl *var) const; + + /// \brief Creates a file-scope variable and returns its instruction. SpirvVariable *createFileVar(const VarDecl *var, llvm::Optional init); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 9394ef5e02..2508a193a4 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2103,12 +2103,17 @@ bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, } if (isRaytracingAccelerationStructure(decl->getType())) { - if (auto *initVal = loadIfGLValue(init)) + if (SpirvInstruction *initVal = loadIfGLValue(init)) { declIdMapper.registerFnVarAlias(decl, initVal); - else + // Track the AS as heap-initialized so diagnoseDescriptorHeapAliasMixing + // can detect any later reassignment (even heap-to-heap, since + // registerFnVarAlias cannot be updated after the fact). + descriptorHeapVarState[decl] = DescriptorHeapVarState::Heap; + } else { emitError("cannot create descriptor heap acceleration structure alias " "from initializer", init->getExprLoc()); + } return true; } @@ -3287,11 +3292,14 @@ SpirvEmitter::tryToAssignToDescriptorHeapAlias( assignExpr->getExprLoc())) return tryToAssignToDescriptorHeapBuffer(assignExpr); - // The assignment was rejected. A heap buffer alias is represented only by its - // index variable, so the normal path has no destination to store into and - // would null-deref; consume the assignment instead. Image aliases do have a - // function variable, so they can fall through to the plain handle store. - if (descriptorHeapBufferAliasVars.count(dstVar)) + // The assignment was rejected. Buffer aliases (index-only, no backing + // function variable) and AS aliases (value registered via registerFnVarAlias, + // not a SpirvVariable) have no valid destination for processAssignment; + // consume the assignment to prevent a null-deref. Image aliases do have a + // backing function variable, so they can fall through to the plain handle + // store. + if (descriptorHeapBufferAliasVars.count(dstVar) || + declIdMapper.hasFnVarAlias(dstVar)) return static_cast(nullptr); return llvm::None; } @@ -5311,19 +5319,29 @@ bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, // Only the resource kinds that use the compile-time alias mechanism can be // miscompiled by a mixed assignment. Other resources are stored into a real // function variable, which merges correctly across control flow. + // RaytracingAccelerationStructure uses registerFnVarAlias and is covered + // here too; its Heap state is set in tryToCreateDescriptorHeapAlias. const QualType dstType = dstVar->getType(); - if (!isRWTexture(dstType) && !isRWBuffer(dstType) && + const bool isASType = isRaytracingAccelerationStructure(dstType); + if (!isASType && !isRWTexture(dstType) && !isRWBuffer(dstType) && !isConstantTextureBuffer(dstType) && !isAKindOfStructuredOrByteBuffer(dstType)) return false; const bool srcIsHeap = isHeapSourcedValue(srcExpr->IgnoreParenCasts()); const bool wasHeap = descriptorHeapImageAliasVars.count(dstVar) || - descriptorHeapBufferAliasVars.count(dstVar); + descriptorHeapBufferAliasVars.count(dstVar) || + (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Heap); const bool wasBound = stateIt != descriptorHeapVarState.end() && stateIt->second == DescriptorHeapVarState::Bound; - const bool mixingDetected = - (srcIsHeap && wasBound) || (!srcIsHeap && wasHeap); + // AS aliases cannot be updated after initialization (registerFnVarAlias is + // frozen), so any reassignment (including heap-to-heap) must be rejected. + // Image and buffer aliases support heap-to-heap reassignment via their + // respective alias-update paths, so only the cross-kind cases are errors. + const bool mixingDetected = (isASType && wasHeap) || + (srcIsHeap && wasBound) || + (!srcIsHeap && wasHeap); if (mixingDetected) { emitError("mixing bound and descriptor heap resources in the same variable " @@ -7242,7 +7260,8 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, emitError("acceleration structure loaded from ResourceDescriptorHeap " "requires the resource heap stride to account for " "acceleration structure descriptors; compile with " - "-fspv-extension=SPV_KHR_ray_tracing or " + "-fspv-extension=SPV_KHR_ray_tracing, " + "-fspv-extension=SPV_NV_ray_tracing, or " "-fspv-extension=SPV_KHR_ray_query", expr->getExprLoc()); return nullptr; diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 8a3460ae8e..0cc8a2e338 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -1707,11 +1707,14 @@ class SpirvEmitter : public ASTConsumer { bool counterUnsupported = false; }; /// Tracks per-variable assignment history for the mixed-alias diagnostic. + /// Heap: the variable was initialized from ResourceDescriptorHeap. For AS + /// variables this is the only state that can follow initialization, + /// since registerFnVarAlias cannot be updated after the fact. /// Bound: the variable has been assigned from a bound resource; a later heap /// assignment to it would be a diagnosable mix. /// Mixed: mixing was already diagnosed; alias recording stays suppressed for /// the remainder of the function so the error fires only once. - enum class DescriptorHeapVarState : uint8_t { Bound, Mixed }; + enum class DescriptorHeapVarState : uint8_t { Heap, Bound, Mixed }; llvm::DenseMap descriptorHeapVarState; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl index 5c94f614b9..26519a5cb8 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl @@ -21,7 +21,7 @@ // when the user lists a ray-tracing/ray-query extension explicitly. The first // run does not, and must fail; the second one does, and must compile. -// CHECK: error: acceleration structure loaded from ResourceDescriptorHeap requires the resource heap stride to account for acceleration structure descriptors; compile with -fspv-extension=SPV_KHR_ray_tracing or -fspv-extension=SPV_KHR_ray_query +// CHECK: error: acceleration structure loaded from ResourceDescriptorHeap requires the resource heap stride to account for acceleration structure descriptors; compile with -fspv-extension=SPV_KHR_ray_tracing, -fspv-extension=SPV_NV_ray_tracing, or -fspv-extension=SPV_KHR_ray_query // OK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR // OK-DAG: %[[AccelSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl index aabb9aa5b9..5d39a64730 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -1,104 +1,115 @@ -// Two test paths share this file; select via -D RT_STAGE: +// Verifies the descriptor-heap array stride when a RaytracingAccelerationStructure +// is present in the resource heap. // -// Path A; Condition 1 (RT stage): -// -T lib_6_6 -D RT_STAGE -fspv-extension=SPV_KHR_ray_tracing -// A closesthit entry point is in the workQueue; shaderModelKindIsRayTracing() -// returns true and noteResourceHeapHasAccelStruct() is called unconditionally. +// When acceleration structures are accessed from ResourceDescriptorHeap, the +// stride formula expands from max(sizeof(image), sizeof(buffer)) to the +// three-way max: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)). +// All resource runtime arrays must share this wider stride so that any slot +// can hold any descriptor type. // -// Path B; Condition 3 (explicit KHR_ray_query extension): -// -T cs_6_6 -E main -fspv-extension=SPV_KHR_ray_query -// No RT stage in workQueue, so Condition 1 is skipped. The -// !spirvOptions.allowedExtensions.empty() guard passes (user listed an -// explicit extension), isExtensionEnabled(KHR_ray_query) is true, and -// noteResourceHeapHasAccelStruct() is called. +// The compiler widens the stride under two conditions: // -// Both paths verify that the shared resource-heap array stride expands to -// max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) -// and that ALL resource runtime arrays share that three-way max stride. +// Path A (RT stage): the shader model is a ray-tracing stage, so any heap +// access may load an acceleration structure. Ray-tracing extensions are +// requested explicitly via -fspv-extension=SPV_KHR_ray_tracing. +// +// Path B (ray query): the SPV_KHR_ray_query extension is explicitly requested +// by the user, signalling that AS descriptors may appear in the heap even +// without a ray-tracing stage. // // Ordering stress test: Texture2D is accessed BEFORE the AS in source order. -// The stride cache must already hold sizeof(accel_struct) when the first -// runtime array type is created so the texture array uses the correct stride. +// If the stride cache were populated before the AS widens it, the texture +// runtime array would be decorated with the narrower two-way max stride. // -// The regression test for the "default-extension-mode" false positive (no -// -fspv-extension flags -> allowedExtensions is empty -> Condition 3 is skipped) -// is sm6_6.descriptorheap.ext.array-stride.hlsl, which must emit exactly 3 -// OpConstantSizeOfEXT (img, buf, sampler - no accel_struct). +// For the no-AS baseline (exactly 3 OpConstantSizeOfEXT: img, buf, sampler) +// see sm6_6.descriptorheap.ext.array-stride.hlsl. -// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefixes=CHECK,RT -// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ // RUN: -spirv %s | FileCheck %s -// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefix=SZRT -// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ // RUN: -spirv %s | FileCheck %s --check-prefix=SZRQ -// --- Types (user-declared + stride-computation placeholders) --------------- -// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR -// CHECK-DAG: %[[Img:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown +// Element (descriptor) types. +// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR + +// Two distinct OpTypeImage types appear in the output: +// +// 1) ImgPlaceholder: a canonical sampled 2D float image (depth=0) used only as +// the operand to OpConstantSizeOfEXT. All image subtypes report the same +// imageDescriptorSize, so the specific depth field is irrelevant to the query. +// +// 2) TexDesc: the actual lowered type for Texture2D (depth=2, +// WithDepth::Unknown), produced by LowerTypeVisitor for sampled textures. +// This is the element type of the texture heap runtime array. +// +// CHECK-DAG: %[[ImgPlaceholder:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown +// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown -// UBuf is not user-declared; it is the canonical placeholder injected by -// getResourceHeapArrayStride() for the buffer descriptor size. -// CHECK-DAG: %[[UBuf:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform +// UBuf is the canonical buffer placeholder injected for the buffer descriptor +// size query; it is not user-declared. +// CHECK-DAG: %[[UBuf:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform -// Sampler only present in path A (closesthit). +// Sampler only present in path A (RT stage). // RT-DAG: %[[Samp:[a-zA-Z0-9_]+]] = OpTypeSampler -// --- Runtime array types --------------------------------------------------- -// CHECK-DAG: %[[AccelArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] -// CHECK-DAG: %[[ImgArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Img]] - +// Heap runtime arrays of the accessed element types. +// CHECK-DAG: %[[AccelArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] +// CHECK-DAG: %[[ImgArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexDesc]] // RT-DAG: %[[SampArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Samp]] -// --- Size operands --------------------------------------------------------- -// CHECK-DAG: %[[ImgSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Img]] -// CHECK-DAG: %[[BufSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBuf]] -// CHECK-DAG: %[[ASSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] +// Resource stride = max(max(image_size, buffer_size), accel_size). +// The size query uses ImgPlaceholder (depth=0); the driver returns the same +// imageDescriptorSize regardless of which image subtype is used as the operand. +// CHECK-DAG: %[[ImgSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[ImgPlaceholder]] +// CHECK-DAG: %[[BufSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBuf]] +// CHECK-DAG: %[[ASSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] // RT-DAG: %[[SampSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Samp]] -// --- Three-way max: max(max(img, buf), accel) identical in both paths ---- -// CHECK-DAG: %[[IB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSz]] %[[BufSz]] -// CHECK-DAG: %[[MaxIB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IB]] %[[ImgSz]] %[[BufSz]] -// CHECK-DAG: %[[IBA:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[MaxIB]] %[[ASSz]] -// CHECK-DAG: %[[Stride:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IBA]] %[[MaxIB]] %[[ASSz]] - -// --- All resource arrays share the three-way-max stride ------------------- -// %[[ImgArr]] is created FIRST (texture before AS in source); most important. -// If the stride cache were populated before noteResourceHeapHasAccelStruct() -// was called, ImgArr would be decorated with a two-way max stride, which -// would not match %[[Stride]] (bound to the three-way max above), failing here. -// CHECK-DAG: OpDecorateId %[[ImgArr]] ArrayStrideIdEXT %[[Stride]] -// CHECK-DAG: OpDecorateId %[[AccelArr]] ArrayStrideIdEXT %[[Stride]] - -// --- Sampler stride is independent (path A only) -------------------------- +// Three-way max: max(max(img, buf), accel), identical in both paths. +// CHECK-DAG: %[[IB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[MaxIB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IB]] %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[IBA:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[MaxIB]] %[[ASSz]] +// CHECK-DAG: %[[Stride:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IBA]] %[[MaxIB]] %[[ASSz]] + +// All resource arrays share the three-way-max stride. +// %[[ImgArr]] is created FIRST (texture before AS in source order); if the +// stride were not yet widened at array-type creation time, this decoration +// would reference the narrower two-way max and the check below would fail. +// CHECK-DAG: OpDecorateId %[[ImgArr]] ArrayStrideIdEXT %[[Stride]] +// CHECK-DAG: OpDecorateId %[[AccelArr]] ArrayStrideIdEXT %[[Stride]] + +// Sampler stride is independent (path A only). // RT-DAG: OpDecorateId %[[SampArr]] ArrayStrideIdEXT %[[SampSz]] -// --- Exact OpConstantSizeOfEXT counts ------------------------------------- -// Path A: img, buf, accel, sampler = 4 +// Exact OpConstantSizeOfEXT counts. +// Path A (RT stage): img, buf, accel, sampler = 4. // SZRT-COUNT-4: OpConstantSizeOfEXT %uint // SZRT-NOT: OpConstantSizeOfEXT %uint -// Path B: img, buf, accel = 3 (no sampler) +// Path B (ray query): img, buf, accel = 3 (no sampler). // SZRQ-COUNT-3: OpConstantSizeOfEXT %uint // SZRQ-NOT: OpConstantSizeOfEXT %uint @@ -109,7 +120,7 @@ struct Attribute { float2 bary; }; [shader("closesthit")] void main(inout Payload payload, in Attribute attr) { - // Ordering stress test: access texture first + // Ordering stress test: access texture first. Texture2D tex = ResourceDescriptorHeap[0]; RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; SamplerState samp = SamplerDescriptorHeap[0]; @@ -133,7 +144,7 @@ RWBuffer output : register(u0); [numthreads(1, 1, 1)] void main(uint3 tid : SV_DispatchThreadID) { - // Ordering stress test: access texture first + // Ordering stress test: access texture first. Texture2D tex = ResourceDescriptorHeap[0]; RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; @@ -151,4 +162,4 @@ void main(uint3 tid : SV_DispatchThreadID) { output[tid.x] = tex.Load(coord) + float4(hit ? 1.0 : 0.0, 0.0, 0.0, 0.0); } -#endif // RT_STAGE \ No newline at end of file +#endif // RT_STAGE From 4aa8557c7d13be9e7035adb9d4fa7fb0689fbcb7 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 21 Aug 2026 10:40:30 -0700 Subject: [PATCH 26/45] fix test --- ...ptorheap.ext.array-stride.accelstruct.hlsl | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl index 5d39a64730..2189a0f618 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -50,7 +50,7 @@ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ -// RUN: -spirv %s | FileCheck %s --check-prefix=SZRQ +// RUN: -spirv %s | FileCheck %s --check-prefixes=SZRQ,NOSAMP // Element (descriptor) types. // CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR @@ -104,14 +104,21 @@ // Sampler stride is independent (path A only). // RT-DAG: OpDecorateId %[[SampArr]] ArrayStrideIdEXT %[[SampSz]] -// Exact OpConstantSizeOfEXT counts. -// Path A (RT stage): img, buf, accel, sampler = 4. -// SZRT-COUNT-4: OpConstantSizeOfEXT %uint -// SZRT-NOT: OpConstantSizeOfEXT %uint - -// Path B (ray query): img, buf, accel = 3 (no sampler). -// SZRQ-COUNT-3: OpConstantSizeOfEXT %uint -// SZRQ-NOT: OpConstantSizeOfEXT %uint +// Path A (RT stage) queries img, buf, accel and sampler; path B (ray query) +// queries img, buf and accel. Each expected query is asserted by a CHECK-DAG +// above, so what remains is to rule out the extra queries that a per-element or +// per-access implementation would emit. +// +// In both paths the lowered Texture2D type must never be measured, because one +// canonical image placeholder covers every image descriptor. +// SZRT-DAG: %[[SZRT_Tex:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// SZRT-NOT: OpConstantSizeOfEXT %uint %[[SZRT_Tex]] +// SZRQ-DAG: %[[SZRQ_Tex:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// SZRQ-NOT: OpConstantSizeOfEXT %uint %[[SZRQ_Tex]] + +// Path B declares no sampler, so no sampler descriptor type reaches the module +// and therefore no sampler size query can be emitted. +// NOSAMP-NOT: OpTypeSampler #ifdef RT_STAGE From 172125e1f41f8a3f862a20fc19f13619759b468d Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 11 Sep 2026 10:02:55 -0700 Subject: [PATCH 27/45] improve comments and convention compliance --- docs/ReleaseNotes.md | 7 +- .../clang/include/clang/SPIRV/SpirvBuilder.h | 32 +++---- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 5 +- tools/clang/lib/SPIRV/DeclResultIdMapper.h | 3 +- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 9 +- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 87 +++++++------------ tools/clang/lib/SPIRV/SpirvEmitter.h | 10 ++- ...ptorheap.ext.array-stride.accelstruct.hlsl | 47 +++++----- 8 files changed, 89 insertions(+), 111 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index cc65e3752a..11d13c394f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -24,9 +24,10 @@ The included licenses apply to the following files: - Extended `SPV_EXT_descriptor_heap` lowering (`-fspv-use-descriptor-heap`) to buffer-like resources (`StructuredBuffer`, `ByteAddressBuffer`, `ConstantBuffer`, `TextureBuffer`, and RW variants) via - `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3` - [#8517](https://github.com/microsoft/DirectXShaderCompiler/pull/8517). - + `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3`. +- Extended `-fspv-use-descriptor-heap` to support `RaytracingAccelerationStructure` + loaded from `ResourceDescriptorHeap`. + Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 521de17277..9d310e2701 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -843,26 +843,26 @@ class SpirvBuilder { SpirvInstruction *op3, SourceLocation loc); /// \brief Record that acceleration structures may occupy the resource heap. - /// Note: Must be called before getResourceHeapArrayStride() (before the - /// code-gen loop in HandleTranslationUnit) so the cached stride is correct - /// on the first call. Calling it later has no effect because the result is - /// frozen after the first getResourceHeapArrayStride() invocation. + /// Must be called before getResourceHeapArrayStride() (before the code-gen + /// loop in HandleTranslationUnit); the result is cached on the first call. + /// Calling it after the first getResourceHeapArrayStride() invocation has + /// no effect. void noteResourceHeapHasAccelStruct() { resourceHeapHasAccelStruct = true; } - /// \brief Returns whether the resource-heap stride accounts for acceleration - /// structure descriptors. Code-gen must reject an acceleration structure heap - /// access when this is false: the decision is made before the code-gen loop - /// and the stride cannot be widened afterwards. + /// \brief Returns whether the resource-heap stride includes the acceleration + /// structure descriptor size. When false, code-gen must reject AS heap + /// access; the stride is frozen after getResourceHeapArrayStride()'s first + /// call. bool resourceHeapStrideIncludesAccelStruct() const { return resourceHeapHasAccelStruct; } - /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays. + /// \brief Shared ArrayStrideIdEXT for resource-heap runtime arrays. /// Default: max(sizeof(image), sizeof(buffer)) - /// With RT: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) - /// Computed via OpSpecConstantOp and cached per module. - /// Note: noteResourceHeapHasAccelStruct() must be called before this if AS - /// may be present (result is frozen on the first call). + /// With RT: max(sizeof(image), sizeof(buffer), sizeof(accel_struct)) + /// Via OpSpecConstantOp; cached per module. + /// Precondition: call noteResourceHeapHasAccelStruct() before first call if + /// AS present. SpirvInstruction *getResourceHeapArrayStride(); /// \brief Shared ArrayStrideIdEXT operand for sampler-heap runtime arrays: @@ -994,9 +994,9 @@ class SpirvBuilder { SpirvInstruction *samplerHeapArrayStride = nullptr; /// Set by noteResourceHeapHasAccelStruct() when HandleTranslationUnit - /// detects that the shader uses ray-tracing features. When true, - /// getResourceHeapArrayStride() extends the stride to include - /// sizeof(acceleration_structure). + /// detects shader using ray-tracing features. + /// When true, getResourceHeapArrayStride() includes + /// sizeof(acceleration_structure). Set by noteResourceHeapHasAccelStruct(). bool resourceHeapHasAccelStruct = false; SpirvDebugInfoNone *debugNone; diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 4660e8a9e0..62246ab0b7 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -1138,8 +1138,8 @@ DeclResultIdMapper::createFnVar(const VarDecl *var, void DeclResultIdMapper::registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr) { - if (varInstr) - registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); + assert(varInstr); + registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); } bool DeclResultIdMapper::hasFnVarAlias(const VarDecl *var) const { @@ -1149,7 +1149,6 @@ bool DeclResultIdMapper::hasFnVarAlias(const VarDecl *var) const { return !isa(info->instr); } - SpirvDebugGlobalVariable *DeclResultIdMapper::createDebugGlobalVariable( SpirvVariable *var, const QualType &type, const SourceLocation &loc, const StringRef &name) { diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.h b/tools/clang/lib/SPIRV/DeclResultIdMapper.h index 9fa590efbf..df92ea96a7 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.h +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.h @@ -288,10 +288,9 @@ class DeclResultIdMapper { /// \brief Registers a function-scope alias to an existing instruction. void registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr); - /// \brief Returns true if the decl was registered via registerFnVarAlias, + /// \brief Returns true if the decl was registered via registerFnVarAlias. bool hasFnVarAlias(const VarDecl *var) const; - /// \brief Creates a file-scope variable and returns its instruction. SpirvVariable *createFileVar(const VarDecl *var, llvm::Optional init); diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index a447092828..b98bb49b70 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2058,12 +2058,12 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { // // Categories and placeholders: // image: - always included; textures lower to OpTypeImage, so this covers - // all non-RT resource kinds. Representative: canonical sampled + // all non-RT resource kinds. Representative: canonical sampled // 2D float image (subtype/storage class do not affect size). // buffer - always included. Representative: Uniform buffer. // accel - included only when noteResourceHeapHasAccelStruct() was called. - // Placeholder: getAccelerationStructureType(). Requires - // OpCapability RayTracingKHR, guaranteed present when + // Placeholder: getAccelerationStructureType(). Requires + // OpCapability RayTracingKHR, guaranteed present when // noteResourceHeapHasAccelStruct() is called. // // VkPhysicalDeviceDescriptorHeapPropertiesEXT reports one size per category @@ -2094,9 +2094,8 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { resourceHeapArrayStride = createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, maxImgBufIsBigger, maxImgBuf, asSize, {}); - } else { + } else resourceHeapArrayStride = maxImgBuf; - } return resourceHeapArrayStride; } diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 2508a193a4..714dc2c246 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -67,9 +67,8 @@ bool isImplicitVarDeclInVkNamespace(const Decl *decl) { return false; } -// Returns true if the given shader model kind is a ray-tracing stage. -// Used to detect whether the resource heap stride must include -// sizeof(acceleration_structure). +// Returns true if k is an RT stage; determines whether the resource-heap +// stride must include sizeof(acceleration_structure). bool shaderModelKindIsRayTracing(hlsl::ShaderModel::Kind k) { switch (k) { case hlsl::ShaderModel::Kind::RayGeneration: @@ -820,38 +819,25 @@ void SpirvEmitter::HandleTranslationUnit(ASTContext &context) { return; } - // Pre-detect whether the resource-heap array stride must include the - // acceleration structure descriptor size. + // Pre-detect whether the resource-heap stride must include the acceleration + // structure size. getResourceHeapArrayStride() caches on first call, so the + // decision must precede any descriptor-heap subscript evaluation. // - // getResourceHeapArrayStride() caches its result on the first call. All - // resource runtime arrays created during code-gen share that one cached - // stride instruction pointer. To make the value correct the decision must - // be made here, before any descriptor-heap subscript expression is evaluated. + // needsAccelStruct is set when any of three conditions holds: + // C1: workQueue has an RT stage. RT stages emit RayTracingKHR, which + // permits OpConstantSizeOfEXT on AccelerationStructureKHR. + // C2: shader explicitly listed KHR_ray_tracing or NV_ray_tracing — + // signals the shader may use acceleration structures via descriptor + // heap. + // C3: shader explicitly listed KHR_ray_query — same rationale. // - // Specifically, needsAccelStruct is set true under any of three conditions: + // isExtensionEnabled() requires the !allowedExtensions.empty() guard: in + // default mode FeatureManager enables KHR_ray_tracing and KHR_ray_query + // by default, causing false positives without it. // - // Condition 1, ray-tracing entry point: any workQueue entry is an RT stage. - // RT stages unconditionally emit OpCapability RayTracingKHR; that capability - // is what permits OpConstantSizeOfEXT on AccelerationStructureKHR, so the - // instruction is safe to emit. - // - // Condition 2, explicit KHR_ray_tracing / NV_ray_tracing extension: a - // compute or graphics shader declared one of these extensions explicitly, - // meaning it may use acceleration structures via descriptor heap. - // - // Condition 3, explicit KHR_ray_query extension: same rationale for - // RayQuery users. - // - // isExtensionEnabled() cannot be used without the guard: in default mode - // (no -fspv-extension flags), FeatureManager enables all by-default - // extensions (including KHR_ray_tracing and KHR_ray_query) causing false - // positives. The guard !spirvOptions.allowedExtensions.empty() reliably - // distinguishes "user-listed" from "allowed by default." - // - // Note: the global-decl pass above runs before this block. HLSL forbids - // descriptor-heap access in global initializers, so - // getResourceHeapArrayStride() cannot be called there. If that restriction is - // ever lifted, this block should be moved before that pass. + // Note: global-decl pass runs before this block. HLSL forbids heap access + // in global initializers (getResourceHeapArrayStride() cannot be called + // there). If that restriction lifts, move this block before that pass. if (spirvOptions.useDescriptorHeap) { bool needsAccelStruct = false; @@ -3292,12 +3278,10 @@ SpirvEmitter::tryToAssignToDescriptorHeapAlias( assignExpr->getExprLoc())) return tryToAssignToDescriptorHeapBuffer(assignExpr); - // The assignment was rejected. Buffer aliases (index-only, no backing - // function variable) and AS aliases (value registered via registerFnVarAlias, - // not a SpirvVariable) have no valid destination for processAssignment; - // consume the assignment to prevent a null-deref. Image aliases do have a - // backing function variable, so they can fall through to the plain handle - // store. + // Assignment rejected. Buffer aliases (index-only, no backing variable) and + // AS aliases (registerFnVarAlias, not SpirvVariable) have no + // processAssignment destination; consume to prevent null-deref. Image aliases + // have a backing variable; fall through to handle store. if (descriptorHeapBufferAliasVars.count(dstVar) || declIdMapper.hasFnVarAlias(dstVar)) return static_cast(nullptr); @@ -5335,10 +5319,9 @@ bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, stateIt->second == DescriptorHeapVarState::Heap); const bool wasBound = stateIt != descriptorHeapVarState.end() && stateIt->second == DescriptorHeapVarState::Bound; - // AS aliases cannot be updated after initialization (registerFnVarAlias is - // frozen), so any reassignment (including heap-to-heap) must be rejected. - // Image and buffer aliases support heap-to-heap reassignment via their - // respective alias-update paths, so only the cross-kind cases are errors. + // AS aliases are frozen (registerFnVarAlias cannot be updated), so any + // reassignment — including heap-to-heap — is rejected. Image/buffer aliases + // support heap-to-heap reassignment; only cross-kind (bound↔heap) is flagged. const bool mixingDetected = (isASType && wasHeap) || (srcIsHeap && wasBound) || (!srcIsHeap && wasHeap); @@ -9499,19 +9482,13 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { const SpirvType * SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType) { - // Apply a client-API-defined byte stride via an ArrayStrideIdEXT decoration. - // The sampler heap holds a single descriptor type, so its stride is the - // sampler descriptor size. The resource heap is a shared flat array in which - // any resource descriptor may sit at any slot, so every resource runtime - // array must use one common stride. - // - // [non-RT shaders]: - // max(sizeof(image), sizeof(buffer)) - // [RT shaders]: - // max(sizeof(image), sizeof(buffer), sizeof(acceleration_structure)) - // - // The stride is determined once before the code-gen loop and cached in - // spvBuilder. + // Apply a client-API-defined byte stride via ArrayStrideIdEXT. + // Sampler heap holds one descriptor type; stride = sampler descriptor size. + // Resource heap is a shared flat array; all runtime arrays share one stride: + // non-RT: max(sizeof(image), sizeof(buffer)) + // RT: max(sizeof(image), sizeof(buffer), + // sizeof(acceleration_structure)) + // Determined once before the code-gen loop; cached in spvBuilder. SpirvInstruction *strideId = isa(elemType) ? spvBuilder.getSamplerHeapArrayStride() : spvBuilder.getResourceHeapArrayStride(); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 0cc8a2e338..1efc897a76 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -1251,10 +1251,12 @@ class SpirvEmitter : public ASTConsumer { /// \brief Diagnoses a local resource variable assigned from both a bound /// resource and ResourceDescriptorHeap. /// - /// Once recorded as an alias, every use is re-lowered as a heap access - /// chain regardless of control flow, correct only if the variable holds - /// a heap descriptor on every reaching path. Mixed sources violate this; - /// reject rather than silently miscompile. + /// The per-resource alias maps (descriptorHeapImageAliasVars, + /// descriptorHeapBufferAliasVars, registerFnVarAlias for AS) record each + /// aliased VarDecl at compile time; every later use is unconditionally + /// re-lowered as a heap access chain, ignoring control flow. Correct only + /// when the variable holds a heap descriptor on every reaching path; + /// diagnose mixed assignments rather than miscompile. /// /// Returns true if rejected (new diagnostic emitted or variable already /// diagnosed); callers need not act, alias-recording helpers check diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl index 2189a0f618..7323f7d237 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -1,51 +1,52 @@ -// Verifies the descriptor-heap array stride when a RaytracingAccelerationStructure -// is present in the resource heap. +// Two test paths share this file; select via -D RT_STAGE: // -// When acceleration structures are accessed from ResourceDescriptorHeap, the -// stride formula expands from max(sizeof(image), sizeof(buffer)) to the -// three-way max: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)). -// All resource runtime arrays must share this wider stride so that any slot -// can hold any descriptor type. +// Path A; Condition 1 (RT stage): +// -T lib_6_6 -D RT_STAGE -fspv-extension=SPV_KHR_ray_tracing +// A closesthit entry point is in the workQueue; shaderModelKindIsRayTracing() +// returns true and noteResourceHeapHasAccelStruct() is called unconditionally. // -// The compiler widens the stride under two conditions: +// Path B; Condition 3 (explicit KHR_ray_query extension): +// -T cs_6_6 -E main -fspv-extension=SPV_KHR_ray_query +// No RT stage in workQueue, so Condition 1 is skipped. The +// !spirvOptions.allowedExtensions.empty() guard passes (user listed an +// explicit extension), isExtensionEnabled(KHR_ray_query) is true, and +// noteResourceHeapHasAccelStruct() is called. // -// Path A (RT stage): the shader model is a ray-tracing stage, so any heap -// access may load an acceleration structure. Ray-tracing extensions are -// requested explicitly via -fspv-extension=SPV_KHR_ray_tracing. -// -// Path B (ray query): the SPV_KHR_ray_query extension is explicitly requested -// by the user, signalling that AS descriptors may appear in the heap even -// without a ray-tracing stage. +// Both paths verify that the shared resource-heap array stride expands to +// max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) +// and that ALL resource runtime arrays share that three-way max stride. // // Ordering stress test: Texture2D is accessed BEFORE the AS in source order. -// If the stride cache were populated before the AS widens it, the texture -// runtime array would be decorated with the narrower two-way max stride. +// The stride cache must already hold sizeof(accel_struct) when the first +// runtime array type is created so the texture array uses the correct stride. // -// For the no-AS baseline (exactly 3 OpConstantSizeOfEXT: img, buf, sampler) -// see sm6_6.descriptorheap.ext.array-stride.hlsl. +// The regression test for the "default-extension-mode" false positive (no +// -fspv-extension flags -> allowedExtensions is empty -> Condition 3 is skipped) +// is sm6_6.descriptorheap.ext.array-stride.hlsl, which must emit exactly 3 +// OpConstantSizeOfEXT (img, buf, sampler - no accel_struct). -// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefixes=CHECK,RT -// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ // RUN: -spirv %s | FileCheck %s -// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefix=SZRT -// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ From 1e0a7842aeb80e36302d2fa4a988e64ff4498557 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Tue, 22 Sep 2026 14:06:57 -0700 Subject: [PATCH 28/45] Implement cross function buffer/image heap alias diagnostics Fixes #8784 --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 138 +++++++++++++++++- tools/clang/lib/SPIRV/SpirvEmitter.h | 53 +++++++ ...ptorheap.ext.counter-ops-direct.error.hlsl | 27 ++++ ...escriptorheap.ext.direct-return.error.hlsl | 37 +++++ ...orheap.ext.image-alias-fn-param.error.hlsl | 37 +++++ ...scriptorheap.ext.image-alias-fn-param.hlsl | 41 ------ ...rheap.ext.image-alias-fn-return.error.hlsl | 51 +++++++ 7 files changed, 339 insertions(+), 45 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops-direct.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.direct-return.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl delete mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 6dc6d9d776..623586182f 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2979,10 +2979,24 @@ void SpirvEmitter::doReturnStmt(const ReturnStmt *stmt) { if (!returnsVoid) { assert(retVal); const Expr *srcExpr = retVal->IgnoreParenCasts(); - if (isHeapSourcedValue(srcExpr)) { + if (spirvOptions.useDescriptorHeap && isHeapSourcedValue(srcExpr)) { if (isDescriptorHeap(srcExpr)) { - // Direct heap subscript: register the heap variable so the declaration - // mapper can resolve it for the return instruction. + // Buffer-like types cannot be returned: the OpBufferPointerEXT + // representation requires VariablePointersStorageBuffer which is + // not supported across function boundaries. + if (isAKindOfStructuredOrByteBuffer(retVal->getType()) || + isConstantTextureBuffer(retVal->getType())) { + emitError("heap buffer cannot be returned from a function; " + "access the buffer element directly at the return site", + retVal->getLocStart()); + spvBuilder.createReturnValue( + spvBuilder.getUndef(curFunction->getReturnType()), + stmt->getReturnLoc()); + return; + } + // Direct heap subscript returning an image resource: register the heap + // variable so the declaration mapper can resolve it for the return + // instruction. const Expr *base = nullptr; getDescriptorHeapOperands(srcExpr, &base, /* index= */ nullptr); const Expr *parentExpr = cast(parentMap->getParent(srcExpr)); @@ -3447,6 +3461,21 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) { return nullptr; } + // A heap image's slot is tracked in the caller's alias state, which the + // callee's parameter doesn't share. Plain reads and writes on the + // parameter still work, since they only need the handle, but an atomic + // needs the slot, so the parameter is flagged for that atomic to report + // the loss. The flag only reaches atomics emitted after this call. + // Heap buffer aliases are rejected below instead: every access through + // one needs the buffer pointer, and passing that pointer would need + // VariablePointersStorageBuffer. + if (spirvOptions.useDescriptorHeap && + !isAKindOfStructuredOrByteBuffer(paramType) && + !isConstantTextureBuffer(paramType) && + isHeapSourcedValue(arg->IgnoreParenCasts())) { + descriptorHeapImageBoundaryLossVars.insert(param); + } + // Get the evaluation info if this argument is referencing some variable // *as a whole*, in which case we can avoid creating the temporary variable // for it if it can act as out parameter. @@ -5509,7 +5538,9 @@ SpirvEmitter::incDecRWACSBufferCounter(const CXXMemberCallExpr *expr, (void)doExpr(object); } - if (isDescriptorHeapCounterUnsupported(object)) { + if (isDescriptorHeapCounterUnsupported(object) || + (spirvOptions.useDescriptorHeap && + isDescriptorHeap(object->IgnoreParenCasts()))) { emitError("counter operations on heap-loaded RWStructuredBuffer are not " "supported with SPV_EXT_descriptor_heap", expr->getCallee()->getExprLoc()); @@ -8508,6 +8539,85 @@ bool SpirvEmitter::isHeapSourcedValue(const Expr *expr) const { descriptorHeapBufferAliasVars.count(var); } +bool SpirvEmitter::isExprStaticallyHeapSourcedImage( + const Expr *expr, llvm::SmallPtrSetImpl &visiting) const { + expr = expr->IgnoreParenCasts(); + if (isDescriptorHeap(expr)) + return true; + const auto *declRefExpr = dyn_cast(expr); + const auto *var = + declRefExpr ? dyn_cast(declRefExpr->getDecl()) : nullptr; + if (!var || !var->getInit() || !visiting.insert(var).second) + return false; + return isExprStaticallyHeapSourcedImage(var->getInit(), visiting); +} + +bool SpirvEmitter::functionReturnsHeapSourcedImage( + const FunctionDecl *fn) const { + fn = fn->getCanonicalDecl(); + auto cached = descriptorHeapImageReturnCache.find(fn); + if (cached != descriptorHeapImageReturnCache.end()) + return cached->second; + + // Memoize before recursing: a function that calls itself (directly or, in + // a future world with recursion, indirectly) must not re-enter this scan. + bool &result = descriptorHeapImageReturnCache[fn]; + result = false; + + if (const Stmt *body = fn->getBody()) { + llvm::SmallVector worklist{body}; + while (!worklist.empty()) { + const Stmt *s = worklist.pop_back_val(); + if (!s) + continue; + if (const auto *ret = dyn_cast(s)) { + const Expr *retVal = ret->getRetValue(); + llvm::SmallPtrSet visiting; + if (retVal && isExprStaticallyHeapSourcedImage(retVal, visiting)) { + result = true; + break; + } + continue; + } + for (const Stmt *child : s->children()) + worklist.push_back(child); + } + } + return result; +} + +bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { + const Expr *e = expr->IgnoreParenCasts(); + + // A call expression evaluated directly (e.g. InterlockedAdd(f(x)[i], ...)) + // never goes through a receiving VarDecl, so check the callee itself. This + // is a pure AST scan (functionReturnsHeapSourcedImage), not a lookup into + // state recorded while emitting the callee: SpirvEmitter drains a + // work queue seeded by the entry point (see HandleTranslationUnit), so a + // callee can be emitted after its caller. Any check keyed on "has the + // callee already run" would miss exactly the cases this function exists + // to catch. + if (const auto *call = dyn_cast(e)) { + if (const auto *fn = call->getDirectCallee()) + return functionReturnsHeapSourcedImage(fn); + return false; + } + + const auto *var = dyn_cast_or_null(getReferencedDef(e)); + if (!var) + return false; + if (descriptorHeapImageBoundaryLossVars.count(var)) + return true; + // A local initialized from a call inherits the call's loss, checked here + // directly (same reason as above: no side-recorded state to consult). + if (const auto *init = var->getInit()) { + if (const auto *call = dyn_cast(init->IgnoreParenCasts())) + if (const auto *fn = call->getDirectCallee()) + return functionReturnsHeapSourcedImage(fn); + } + return false; +} + void SpirvEmitter::getDescriptorHeapOperands(const Expr *expr, const Expr **base, const Expr **index) { @@ -11101,6 +11211,26 @@ SpirvEmitter::processIntrinsicInterlockedMethod(const CallExpr *expr, } auto *coordInstr = doExpr(index); + // An atomic on a heap image needs its heap slot, which doesn't survive + // a function call. Without this check, the fallback below emits + // OpImageTexelPointer on a Function-class copy, which fails + // VUID-StandaloneSpirv-OpTypeImage-06924 with no diagnostic. The loss + // is traced to the crossing rather than inferred from Function storage + // class, because bound images copied into locals or parameters are + // Function-class too and legalize fine. Detection is best-effort. + // TODO(#8784): implement cross-function heap image propagation; once + // the slot itself can cross the boundary, remove this check and it's + // tracker + if (spirvOptions.useDescriptorHeap && + isDescriptorHeapImageBoundaryLoss(base)) { + emitError( + "interlocked operation on a heap-backed RWTexture passed to or " + "returned from a helper function is not supported; use the " + "image directly at the call site", + dest->getExprLoc()); + return nullptr; + } + if (spirvOptions.useDescriptorHeap) { const Expr *heapBase = base->IgnoreParenCasts(); auto access = descriptorHeapImageAccesses.find(heapBase); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 8a3460ae8e..10d3f8df91 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -35,6 +35,7 @@ #include "clang/SPIRV/SpirvContext.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" #include "ConstEvaluator.h" #include "DeclResultIdMapper.h" @@ -295,6 +296,37 @@ class SpirvEmitter : public ASTConsumer { /// this value heap-sourced?" so that alias-to-alias flows are recognized. bool isHeapSourcedValue(const Expr *expr) const; + /// \brief Returns true if expr is a heap image whose heap slot was lost + /// crossing a user function call, as a parameter or a return value. + /// + /// The slot is recorded per variable in the function that indexed the heap + /// and isn't propagated across calls, so after a crossing only the + /// image handle remains. Detection is best-effort: it recognizes flagged + /// parameters and values returned from calls, not every copy or + /// reassignment of them. expr's type isn't checked, since heap buffer + /// aliases are rejected at the crossing itself. + bool isDescriptorHeapImageBoundaryLoss(const Expr *expr) const; + + /// \brief Returns true if a return statement in fn yields a statically + /// heap-sourced value (see isExprStaticallyHeapSourcedImage). + /// + /// Scans the AST rather than recording state while emitting fn, because + /// the work queue can emit fn after the caller that needs the answer. + /// Memoized in descriptorHeapImageReturnCache. + bool functionReturnsHeapSourcedImage(const FunctionDecl *fn) const; + + /// \brief Returns true if expr is a descriptor heap subscript, or refers to + /// a VarDecl initialized from one, eg: + /// RWTexture2D a = ResourceDescriptorHeap[i]; + /// RWTexture2D b = a; // b + /// + /// Reads initializers rather than descriptorHeapImageAliasVars, so it can + /// answer for a function that hasn't been emitted yet. Tradeoff is that + /// later assignments aren't seen. `visiting` guards against revisiting a + /// VarDecl. + bool isExprStaticallyHeapSourcedImage( + const Expr *expr, llvm::SmallPtrSetImpl &visiting) const; + void getDescriptorHeapOperands(const Expr *expr, const Expr **base, const Expr **index); @@ -1724,6 +1756,27 @@ class SpirvEmitter : public ASTConsumer { llvm::DenseMap descriptorHeapBufferAliasVars; + /// Parameters whose argument, at some call site, was a heap-sourced image + /// (see isHeapSourcedValue): the slot lives in the caller's + /// descriptorHeapImageAliasVars, keyed on the caller's VarDecl, and is + /// invisible to the callee's parameter. Plain OpImageRead/OpImageWrite + /// remain valid on the parameter (the loaded handle alone is sufficient), + /// so this is only consulted lazily, when an atomic needs the heap slot + /// for OpImageTexelPointer. See isDescriptorHeapImageBoundaryLoss. + /// + /// Only parameters are recorded here: a local initialized from a + /// heap-returning call is instead detected on demand by + /// isDescriptorHeapImageBoundaryLoss re-deriving it from the local's own + /// initializer, because whether the call is heap-returning can depend on + /// a callee not yet emitted (see functionReturnsHeapSourcedImage) at the + /// point the local's declaration is processed. + llvm::DenseSet descriptorHeapImageBoundaryLossVars; + + /// Memoization cache for functionReturnsHeapSourcedImage, keyed on each + /// FunctionDecl's canonical declaration. + mutable llvm::DenseMap + descriptorHeapImageReturnCache; + /// The source location of a push constant block we have previously seen. /// Invalid means no push constant blocks defined thus far. SourceLocation seenPushConstantAt; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops-direct.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops-direct.error.hlsl new file mode 100644 index 0000000000..d2cdaef07a --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.counter-ops-direct.error.hlsl @@ -0,0 +1,27 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_INCREMENT %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// Verifies: counter ops on a direct-cast heap buffer are rejected with the same +// diagnostic as the alias form in counter-ops.error.hlsl. +// +// The direct form has no alias VarDecl, so the descriptorHeapBufferAliasVars +// lookup misses and the call used to fall through to the unrelated "Cannot +// access associated counter variable for an array of buffers in a struct" +// fatal error. + +// CHECK: counter operations on heap-loaded RWStructuredBuffer are not supported with SPV_EXT_descriptor_heap + +RWByteAddressBuffer outputBytes : register(u0); + +[numthreads(1, 1, 1)] +void main() { +#ifdef TEST_INCREMENT + uint value = + ((RWStructuredBuffer)ResourceDescriptorHeap[0]).IncrementCounter(); +#else + uint value = + ((RWStructuredBuffer)ResourceDescriptorHeap[0]).DecrementCounter(); +#endif + + outputBytes.Store(0, value); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.direct-return.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.direct-return.error.hlsl new file mode 100644 index 0000000000..db9faed5fc --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.direct-return.error.hlsl @@ -0,0 +1,37 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_EXPLICIT_CAST %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_BYTEADDRESS %s 2>&1 | FileCheck %s + +// Verifies: returning a direct heap subscript for a buffer-like type is +// rejected, in both the implicit and explicit cast forms. +// +// This form has no alias VarDecl, so it skips the alias-return guard in +// alias-return.hlsl. It used to carry the OpBufferPointerEXT value across +// OpReturnValue without VariablePointersStorageBuffer, producing invalid +// SPIR-V with no diagnostic at all. +// +// TODO(#8784): accept once cross-function heap buffer propagation is +// implemented. + +// CHECK: heap buffer cannot be returned from a function + +RWByteAddressBuffer outputBytes : register(u0); + +#ifdef TEST_BYTEADDRESS +ByteAddressBuffer get() { return ResourceDescriptorHeap[40]; } +#elif defined(TEST_EXPLICIT_CAST) +StructuredBuffer get() { + return (StructuredBuffer)ResourceDescriptorHeap[40]; +} +#else +StructuredBuffer get() { return ResourceDescriptorHeap[40]; } +#endif + +[numthreads(1, 1, 1)] +void main() { +#ifdef TEST_BYTEADDRESS + outputBytes.Store(0, get().Load(0)); +#else + outputBytes.Store(0, get()[0]); +#endif +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl new file mode 100644 index 0000000000..8d108b23fb --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl @@ -0,0 +1,37 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_INOUT %s 2>&1 | FileCheck %s + +// Verifies: a heap image alias passed to a function and used in an atomic is +// rejected, for both 'in' and 'inout' parameters. +// +// The argument is copied into a Function-class "param.var.*" variable and the +// callee's parameter is not an alias VarDecl, so the descriptor index cannot be +// recovered. The old fallback emitted OpImageTexelPointer on the parameter, +// which pinned it as a variable and kept an image-typed OpStore alive, failing +// validation with [VUID-StandaloneSpirv-OpTypeImage-06924] and no diagnostic. +// +// Plain reads and writes through the same parameter still work, because the +// loaded handle is sufficient — see alias-fn-readwrite.hlsl. +// +// TODO(#8784): accept once the descriptor index is propagated across function +// boundaries. + +// CHECK: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported + +RWByteAddressBuffer outputBytes : register(u0); + +#ifdef TEST_INOUT +void bump(inout RWTexture2D t, uint2 coord, out uint orig) { +#else +void bump(RWTexture2D t, uint2 coord, out uint orig) { +#endif + InterlockedAdd(t[coord], 1, orig); +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture2D tex = ResourceDescriptorHeap[40]; + uint r; + bump(tex, tid.xy, r); + outputBytes.Store(0, r); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl deleted file mode 100644 index c6c57c12c9..0000000000 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.hlsl +++ /dev/null @@ -1,41 +0,0 @@ -// XFAIL: * -// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s | FileCheck %s - -// XFAIL: heap image alias passed to a function and used in an atomic. -// Same failure as alias-return.hlsl: [VUID-StandaloneSpirv-OpTypeImage-06924]. -// -// The argument is copied into a Function-class "param.var.*" variable. The -// callee has no entry in the image alias map for its parameter, so the atomic -// falls back to OpImageTexelPointer on the parameter, pinning it as a variable -// and keeping the image-typed OpStore alive through validation. -// -// Plain reads/writes through the same parameter work — see alias-fn-readwrite.hlsl. -// Note: function-params.hlsl only checks the pre-legalization form (-fcgl), -// where the validator relaxes this rule, so it does not catch this failure. -// -// TODO: propagate descriptor index across function boundaries, then remove XFAIL. - -// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant -// CHECK-DAG: %[[RWTexType:[a-zA-Z0-9_]+]] = OpTypeImage %uint 2D 2 0 0 2 R32ui -// CHECK-DAG: %[[RWTexArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[RWTexType]] -// CHECK-DAG: %[[UntypedImage:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR Image - -// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant - -RWByteAddressBuffer outputBytes : register(u0); - -void bump(RWTexture2D t, uint2 coord, out uint orig) { - // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[RWTexArray]] %[[ResourceHeap]] %uint_40 - // CHECK-NOT: OpImageTexelPointer - // CHECK: %[[TexelPtr:[a-zA-Z0-9_]+]] = OpUntypedImageTexelPointerEXT %[[UntypedImage]] %[[RWTexType]] %[[Desc]] - // CHECK: OpAtomicIAdd %uint %[[TexelPtr]] - InterlockedAdd(t[coord], 1, orig); -} - -[numthreads(1, 1, 1)] -void main(uint3 tid : SV_DispatchThreadID) { - RWTexture2D tex = ResourceDescriptorHeap[40]; - uint r; - bump(tex, tid.xy, r); - outputBytes.Store(0, r); -} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl new file mode 100644 index 0000000000..958e83230d --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl @@ -0,0 +1,51 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_DIRECT_USE %s 2>&1 | FileCheck %s + +// Verifies: a heap image alias obtained from a function return value and then +// used in an atomic is rejected, for both assignment-then-atomic and direct +// use of the return value. +// +// When the image is returned from a helper the descriptor-index slot is not +// propagated to the caller. The old code fell through to the bound-resource +// fallback which emits OpImageTexelPointer on a Function-class image pointer, +// failing SPIR-V validation with +// [VUID-StandaloneSpirv-OpTypeImage-06924] and no diagnostic. +// +// The storage-class of the resolved base instruction is the natural predicate: +// - bound global -> UniformConstant -> fallback is valid +// - returned/local alias -> Function -> diagnose before emitting +// +// Plain reads and writes through the returned alias still work because +// OpImageRead/OpImageWrite only need the handle value; only atomics +// (OpImageTexelPointer) are affected by this limitation. +// +// TODO(#8784): accept once the descriptor index is propagated across function +// boundaries. + +// CHECK: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported + +RWByteAddressBuffer outputBytes : register(u0); + +RWTexture2D makeAlias(uint slot) { + RWTexture2D tex = ResourceDescriptorHeap[slot]; + return tex; +} + +#ifndef TEST_DIRECT_USE +// Case 1: assign return value to local, then atomic on local. +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture2D tex = makeAlias(40); + uint r; + InterlockedAdd(tex[tid.xy], 1, r); + outputBytes.Store(0, r); +} +#else +// Case 2: atomic directly on the return-value subscript. +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + uint r; + InterlockedAdd(makeAlias(40)[tid.xy], 1, r); + outputBytes.Store(0, r); +} +#endif From ac7434a00878481c7123714655d8cae214acdcfe Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 23 Sep 2026 14:54:42 -0700 Subject: [PATCH 29/45] documentation fix --- docs/SPIR-V.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/SPIR-V.rst b/docs/SPIR-V.rst index 18b6609d61..ef0dd3a3a2 100644 --- a/docs/SPIR-V.rst +++ b/docs/SPIR-V.rst @@ -2163,14 +2163,15 @@ associated counter operations such as ``IncrementCounter`` and ``DecrementCounter`` emit a diagnostic because the native descriptor heap path does not recover an associated counter descriptor. -A local resource variable initialized from a heap access is resolved entirely at -compile time: the variable is recorded as an alias for the heap index, and every -later use is re-lowered as a fresh access chain rather than as a load of a stored -descriptor handle. This is sound only when the variable holds a heap descriptor -on every path that reaches the use. DXC therefore rejects a variable that holds -both a bound resource and a heap descriptor, whether through a conditional -assignment, a reassignment back to a bound resource, or an assignment inside a -loop:: +Buffer-like local variables initialized from a heap access are tracked by their +heap index, and later uses are re-lowered as fresh access chains. Image +variables retain their loaded descriptor handle for ordinary reads and writes; +the tracked heap index is used when an atomic operation requires +``OpUntypedImageTexelPointerEXT``. Acceleration-structure aliases retain the +initially loaded handle. Because index-tracked aliases do not model +control-flow merges, DXC rejects a variable that holds both a bound resource +and a heap descriptor, whether through a conditional assignment, a +reassignment back to a bound resource, or an assignment inside a loop:: error: mixing bound and descriptor heap resources in the same variable is not supported with SPV_EXT_descriptor_heap From 6a9a5ff39aaf77bc57cfa3f895c65baf7f1f3afd Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 23 Sep 2026 15:40:49 -0700 Subject: [PATCH 30/45] reject passing heap buffers to funcs --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 42 ++++++++++--------- ...heap.ext.buffer-direct-fn-param.error.hlsl | 17 ++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-direct-fn-param.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 623586182f..03e48e42ee 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -3476,30 +3476,34 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) { descriptorHeapImageBoundaryLossVars.insert(param); } + // Any heap-sourced buffer argument (a direct heap subscript, or a + // reference to a variable recorded in descriptorHeapBufferAliasVars) + // has no VarDecl in astDecls to load a pointer from, and passing the + // OpBufferPointerEXT pointer itself would need + // VariablePointersStorageBuffer + callee parameter type changes. + // Reject before doExpr lowers it into an invalid function argument. + // TODO: implement full buffer-alias function-call support + // (VariablePointersStorageBuffer + matching createFnParam type). + if (spirvOptions.useDescriptorHeap && + (isAKindOfStructuredOrByteBuffer(paramType) || + isConstantTextureBuffer(paramType)) && + isHeapSourcedValue(arg->IgnoreParenCasts())) { + emitError("heap buffer alias cannot be passed to a user function; " + "access the buffer element directly at the call site", + arg->getLocStart()); + + // Return a typed undef so downstream consumers remain valid + QualType retTy = callExpr->getCallReturnType(astContext); + if (retTy->isVoidType()) + return nullptr; + return spvBuilder.getUndef(retTy); + } + // Get the evaluation info if this argument is referencing some variable // *as a whole*, in which case we can avoid creating the temporary variable // for it if it can act as out parameter. SpirvInstruction *argInfo = nullptr; if (const auto *declRefExpr = dyn_cast(arg)) { - // Buffer alias vars are not in astDecls (getDeclEvalInfo would crash); - // passing by value also requires VariablePointersStorageBuffer + callee - // parameter type changes (not yet implemented). Emit diagnostic instead. - // TODO: implement full buffer-alias function-call support - // (VariablePointersStorageBuffer + matching createFnParam type). - const auto *var = dyn_cast(declRefExpr->getDecl()); - if (var && descriptorHeapBufferAliasVars.count(var)) { - emitError("heap buffer alias cannot be passed to a user function; " - "access the buffer element directly at the call site", - arg->getLocStart()); - // emitError does not halt codegen; nullptr propagates to spvBuilder - // causing an access violation. Return a typed undef so downstream - // consumers remain valid; the diagnostic rejects the shader even if - // codegen continues with the placeholder. - QualType retTy = callExpr->getCallReturnType(astContext); - if (retTy->isVoidType()) - return nullptr; - return spvBuilder.getUndef(retTy); - } argInfo = declIdMapper.getDeclEvalInfo(declRefExpr->getDecl(), arg->getLocStart()); } diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-direct-fn-param.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-direct-fn-param.error.hlsl new file mode 100644 index 0000000000..6686c3c633 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.buffer-direct-fn-param.error.hlsl @@ -0,0 +1,17 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// Verifies that passing a heap buffer directly, via a cast at the call site +// rather than through a local alias variable, also emits a diagnostic +// instead of lowering the OpBufferPointerEXT into an invalid function +// argument. + +// CHECK: heap buffer alias cannot be passed to a user function + +RWByteAddressBuffer outputBytes : register(u0); + +uint consume(StructuredBuffer buf) { return buf[0]; } + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + outputBytes.Store(0, consume((StructuredBuffer)ResourceDescriptorHeap[0])); +} From b4ccd364b55a7c6c4fe0c3fd7dcf53c35abfe919 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 25 Sep 2026 13:29:22 -0700 Subject: [PATCH 31/45] Trace heap image return value --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 99 ++++++++++++++----- tools/clang/lib/SPIRV/SpirvEmitter.h | 38 ++++--- ...t.image-alias-fn-return-forward.error.hlsl | 58 +++++++++++ ...rheap.ext.image-alias-fn-return.error.hlsl | 7 +- 4 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 03e48e42ee..8440639eca 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -8548,12 +8548,52 @@ bool SpirvEmitter::isExprStaticallyHeapSourcedImage( expr = expr->IgnoreParenCasts(); if (isDescriptorHeap(expr)) return true; + // expr may itself be a call to another function that returns a + // heap-sourced image, e.g. `return makeHeapImage();`. + if (const auto *call = dyn_cast(expr)) { + if (const auto *fn = call->getDirectCallee()) + return functionReturnsHeapSourcedImage(fn); + return false; + } const auto *declRefExpr = dyn_cast(expr); const auto *var = declRefExpr ? dyn_cast(declRefExpr->getDecl()) : nullptr; - if (!var || !var->getInit() || !visiting.insert(var).second) + if (!var || !visiting.insert(var).second) + return false; + if (var->getInit() && + isExprStaticallyHeapSourcedImage(var->getInit(), visiting)) + return true; + // var may also become heap-sourced through a later assignment rather than + // its initializer, e.g. `RWTexture2D t; t = + // ResourceDescriptorHeap[i];`. + return anyAssignmentToVarSatisfies(var, [this, &visiting](const Expr *rhs) { + return isExprStaticallyHeapSourcedImage(rhs, visiting); + }); +} + +bool SpirvEmitter::anyAssignmentToVarSatisfies( + const VarDecl *var, llvm::function_ref pred) const { + const auto *fn = + dyn_cast_or_null(var->getParentFunctionOrMethod()); + if (!fn || !fn->getBody()) return false; - return isExprStaticallyHeapSourcedImage(var->getInit(), visiting); + llvm::SmallVector worklist{fn->getBody()}; + while (!worklist.empty()) { + const Stmt *s = worklist.pop_back_val(); + if (!s) + continue; + if (const auto *assign = dyn_cast(s)) { + if (assign->getOpcode() == BO_Assign) { + const auto *lhsRef = + dyn_cast(assign->getLHS()->IgnoreParenCasts()); + if (lhsRef && lhsRef->getDecl() == var && pred(assign->getRHS())) + return true; + } + } + for (const Stmt *child : s->children()) + worklist.push_back(child); + } + return false; } bool SpirvEmitter::functionReturnsHeapSourcedImage( @@ -8563,11 +8603,16 @@ bool SpirvEmitter::functionReturnsHeapSourcedImage( if (cached != descriptorHeapImageReturnCache.end()) return cached->second; - // Memoize before recursing: a function that calls itself (directly or, in - // a future world with recursion, indirectly) must not re-enter this scan. - bool &result = descriptorHeapImageReturnCache[fn]; - result = false; + // Seed the cache with false before recursing, so a function that calls + // itself directly or indirectly (via the CallExpr case in + // isExprStaticallyHeapSourcedImage) does not re-enter this scan. Write via + // a local and a second map lookup, rather than holding a reference across + // the scan: the recursive calls below insert other FunctionDecls into the + // same DenseMap, which can resize and invalidate any reference held into + // it. + descriptorHeapImageReturnCache[fn] = false; + bool result = false; if (const Stmt *body = fn->getBody()) { llvm::SmallVector worklist{body}; while (!worklist.empty()) { @@ -8587,39 +8632,41 @@ bool SpirvEmitter::functionReturnsHeapSourcedImage( worklist.push_back(child); } } + descriptorHeapImageReturnCache[fn] = result; return result; } bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { const Expr *e = expr->IgnoreParenCasts(); - // A call expression evaluated directly (e.g. InterlockedAdd(f(x)[i], ...)) - // never goes through a receiving VarDecl, so check the callee itself. This - // is a pure AST scan (functionReturnsHeapSourcedImage), not a lookup into - // state recorded while emitting the callee: SpirvEmitter drains a - // work queue seeded by the entry point (see HandleTranslationUnit), so a - // callee can be emitted after its caller. Any check keyed on "has the - // callee already run" would miss exactly the cases this function exists - // to catch. - if (const auto *call = dyn_cast(e)) { - if (const auto *fn = call->getDirectCallee()) - return functionReturnsHeapSourcedImage(fn); + // expr may be a call evaluated directly (e.g. InterlockedAdd(f(x)[i], + // ...)), or a local (transitively) initialized or reassigned from one; a + // call's return crosses the same boundary either way. This is a pure AST + // scan (functionReturnsHeapSourcedImage), not a lookup into state recorded + // while emitting the callee: SpirvEmitter drains a work queue seeded by + // the entry point (see HandleTranslationUnit), so a callee can be emitted + // after its caller. A local sourced directly from a heap subscript + // instead, with no call involved, is NOT a loss: no boundary was crossed, + // so descriptorHeapImageAliasVars, populated by that same-function + // declaration or assignment, already has what's needed. + auto crossesThroughCall = [this](const Expr *x) { + if (const auto *call = dyn_cast(x->IgnoreParenCasts())) + if (const auto *fn = call->getDirectCallee()) + return functionReturnsHeapSourcedImage(fn); return false; - } + }; + if (crossesThroughCall(e)) + return true; const auto *var = dyn_cast_or_null(getReferencedDef(e)); if (!var) return false; + // Parameters are flagged explicitly at their call sites (see processCall). if (descriptorHeapImageBoundaryLossVars.count(var)) return true; - // A local initialized from a call inherits the call's loss, checked here - // directly (same reason as above: no side-recorded state to consult). - if (const auto *init = var->getInit()) { - if (const auto *call = dyn_cast(init->IgnoreParenCasts())) - if (const auto *fn = call->getDirectCallee()) - return functionReturnsHeapSourcedImage(fn); - } - return false; + if (var->getInit() && crossesThroughCall(var->getInit())) + return true; + return anyAssignmentToVarSatisfies(var, crossesThroughCall); } void SpirvEmitter::getDescriptorHeapOperands(const Expr *expr, diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 10d3f8df91..cd1e506109 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -300,13 +300,23 @@ class SpirvEmitter : public ASTConsumer { /// crossing a user function call, as a parameter or a return value. /// /// The slot is recorded per variable in the function that indexed the heap - /// and isn't propagated across calls, so after a crossing only the - /// image handle remains. Detection is best-effort: it recognizes flagged - /// parameters and values returned from calls, not every copy or - /// reassignment of them. expr's type isn't checked, since heap buffer - /// aliases are rejected at the crossing itself. + /// and isn't propagated across calls, so after a crossing only the image + /// handle remains. A value sourced directly from a heap subscript within + /// the current function, with no call involved, is NOT a loss: no + /// boundary was crossed, so descriptorHeapImageAliasVars (populated by + /// that same-function declaration or assignment) already has what's + /// needed. expr's type isn't checked, since heap buffer aliases are + /// rejected at the crossing itself. bool isDescriptorHeapImageBoundaryLoss(const Expr *expr) const; + /// \brief Scans var's enclosing function for an assignment `var = rhs;` + /// anywhere in the body, calling pred(rhs) for each and returning true on + /// the first match. Order/control-flow insensitive: any assignment + /// anywhere in the function counts, not only ones that reach a particular + /// use. + bool anyAssignmentToVarSatisfies( + const VarDecl *var, llvm::function_ref pred) const; + /// \brief Returns true if a return statement in fn yields a statically /// heap-sourced value (see isExprStaticallyHeapSourcedImage). /// @@ -315,15 +325,19 @@ class SpirvEmitter : public ASTConsumer { /// Memoized in descriptorHeapImageReturnCache. bool functionReturnsHeapSourcedImage(const FunctionDecl *fn) const; - /// \brief Returns true if expr is a descriptor heap subscript, or refers to - /// a VarDecl initialized from one, eg: + /// \brief Returns true if expr is a descriptor heap subscript, a call to a + /// function that returns a heap-sourced image (see + /// functionReturnsHeapSourcedImage), or refers to a VarDecl that is + /// (transitively) initialized or assigned from any of the above, eg: /// RWTexture2D a = ResourceDescriptorHeap[i]; - /// RWTexture2D b = a; // b + /// RWTexture2D b; + /// b = a; // b /// - /// Reads initializers rather than descriptorHeapImageAliasVars, so it can - /// answer for a function that hasn't been emitted yet. Tradeoff is that - /// later assignments aren't seen. `visiting` guards against revisiting a - /// VarDecl. + /// Reads initializers and assignments from the AST rather than + /// descriptorHeapImageAliasVars, so it can answer for a function that + /// hasn't been emitted yet. Assignment search is order/control-flow + /// insensitive (any assignment to the VarDecl anywhere in its function + /// counts). `visiting` guards against revisiting a VarDecl. bool isExprStaticallyHeapSourcedImage( const Expr *expr, llvm::SmallPtrSetImpl &visiting) const; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl new file mode 100644 index 0000000000..7d2d25fa2e --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl @@ -0,0 +1,58 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck --check-prefix=FORWARD %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_REASSIGN %s 2>&1 | FileCheck --check-prefix=REASSIGN %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_LOCAL_REASSIGN_FROM_CALL %s 2>&1 | FileCheck --check-prefix=LOCAL %s + +// Verifies three cases that must all be diagnosed the same as a direct +// `return ;`, not silently fall +// through to an invalid OpImageTexelPointer: +// +// Case FORWARD - a function's return value is itself a call to another +// function that returns a heap-sourced image +// (functionReturnsHeapSourcedImage's CallExpr case). +// Case REASSIGN - the returned variable is heap-sourced via a later +// assignment rather than its initializer +// (isExprStaticallyHeapSourcedImage's anyAssignmentToVarSatisfies +// fallback). +// Case LOCAL - a local is reassigned from a call, then used in an atomic in +// the SAME function as the reassignment +// (isDescriptorHeapImageBoundaryLoss's own anyAssignmentToVarSatisfies +// fallback, not functionReturnsHeapSourcedImage's). This must stay +// distinct from a local reassigned directly from a heap subscript, which +// is NOT a loss (see sm6_6.descriptorheap.ext.rwtexture-atomics.hlsl's +// reassignment case). + +// FORWARD: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// REASSIGN: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// LOCAL: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported + +RWByteAddressBuffer outputBytes : register(u0); + +RWTexture2D makeAliasCore(uint slot) { + RWTexture2D tex = ResourceDescriptorHeap[slot]; + return tex; +} + +#if defined(TEST_REASSIGN) +RWTexture2D makeAlias(uint slot) { + RWTexture2D tex; + tex = ResourceDescriptorHeap[slot]; + return tex; +} +#elif !defined(TEST_LOCAL_REASSIGN_FROM_CALL) +// Forwards makeAliasCore's heap-sourced return value without touching a +// ResourceDescriptorHeap subscript itself. +RWTexture2D makeAlias(uint slot) { return makeAliasCore(slot); } +#endif + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + uint r; +#ifdef TEST_LOCAL_REASSIGN_FROM_CALL + RWTexture2D tex; + tex = makeAliasCore(40); +#else + RWTexture2D tex = makeAlias(40); +#endif + InterlockedAdd(tex[tid.xy], 1, r); + outputBytes.Store(0, r); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl index 958e83230d..e8a4023176 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl @@ -11,9 +11,10 @@ // failing SPIR-V validation with // [VUID-StandaloneSpirv-OpTypeImage-06924] and no diagnostic. // -// The storage-class of the resolved base instruction is the natural predicate: -// - bound global -> UniformConstant -> fallback is valid -// - returned/local alias -> Function -> diagnose before emitting +// functionReturnsHeapSourcedImage scans the callee's AST for a return +// statement whose value is statically heap-sourced, independent of codegen +// order (the callee may not have been emitted yet): diagnose before +// emitting whenever that scan says yes. // // Plain reads and writes through the returned alias still work because // OpImageRead/OpImageWrite only need the handle value; only atomics From 6c6d2c5a242a5d58d68396adf27c1844d7304c86 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 28 Sep 2026 11:39:08 -0700 Subject: [PATCH 32/45] Normalized chained assignment RHS values and fixed image alias test --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 79 +++++++++++++++++-- tools/clang/lib/SPIRV/SpirvEmitter.h | 19 +++++ ...descriptorheap.ext.alias-fn-readwrite.hlsl | 3 +- ...ptorheap.ext.chained-assignment.error.hlsl | 44 +++++++++++ 4 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.chained-assignment.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 8440639eca..1486716b7a 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -3232,6 +3232,43 @@ SpirvEmitter::tryToAssignToDescriptorHeapAlias( return llvm::None; } +/// \brief All descriptor-heap handling for a BO_Assign expression. +/// Evaluates the RHS itself and returns it via *rhs, since order matters here: +/// image-alias tracking below needs the RHS already evaluated (reads side +/// effect of it), while a buffer alias must skip normal evaluation entirely. +/// Returns true if *result is the assignment's final value, to be returned +/// as-is; false if the caller should run normal assignment codegen with +/// *rhs. +bool SpirvEmitter::tryHandleDescriptorHeapAssignment(const BinaryOperator *expr, + SpirvInstruction **result, + SpirvInstruction **rhs) { + *rhs = nullptr; + if (!spirvOptions.useDescriptorHeap) + return false; + + // Chained RHS (a = b = ResourceDescriptorHeap[0];) hides the heap source + // from the alias tracking below, leaving lhs untracked and regressing to + // invalid SPIR-V with no diagnostic: VUID-StandaloneSpirv-OpTypeImage-06924 + // for an image atomic, or an invalid OpLoad of the whole buffer for a + // buffer alias. + if (diagnoseChainedDescriptorHeapAssignment(expr)) { + *result = spvBuilder.getUndef(expr->getType()); + return true; + } + + if (llvm::Optional aliasResult = + tryToAssignToDescriptorHeapAlias(expr)) { + *result = aliasResult.getValue(); + return true; + } + + *rhs = loadIfGLValue(expr->getRHS()); + // descriptorHeapImageAccesses (Expr*-keyed) is populated as a side effect + // of the loadIfGLValue call just above; this lookup depends on that. + tryToAssignDescriptorHeapImageAlias(expr->getLHS(), expr->getRHS()); + return false; +} + SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { const auto opcode = expr->getOpcode(); @@ -3241,13 +3278,12 @@ SpirvInstruction *SpirvEmitter::doBinaryOperator(const BinaryOperator *expr) { // Update counter variable associated with lhs of assignments tryToAssignCounterVar(expr->getLHS(), expr->getRHS()); - if (llvm::Optional aliasResult = - tryToAssignToDescriptorHeapAlias(expr)) - return aliasResult.getValue(); - - SpirvInstruction *rhs = loadIfGLValue(expr->getRHS()); - tryToAssignDescriptorHeapImageAlias(expr->getLHS(), expr->getRHS()); - + SpirvInstruction *heapResult = nullptr; + SpirvInstruction *rhs = nullptr; + if (tryHandleDescriptorHeapAssignment(expr, &heapResult, &rhs)) + return heapResult; + if (!rhs) + rhs = loadIfGLValue(expr->getRHS()); return processAssignment(expr->getLHS(), rhs, /*isCompoundAssignment=*/false, nullptr, expr->getSourceRange()); @@ -8533,6 +8569,35 @@ bool SpirvEmitter::isDescriptorHeap(const Expr *expr) const { isSamplerDescriptorHeap(objectType); } +bool SpirvEmitter::diagnoseChainedDescriptorHeapAssignment( + const BinaryOperator *expr) { + const QualType lhsType = expr->getLHS()->getType(); + if (!isRWTexture(lhsType) && !isRWBuffer(lhsType) && + !isConstantTextureBuffer(lhsType) && + !isAKindOfStructuredOrByteBuffer(lhsType)) + return false; + + // Peel through any nesting (a = b = c = X;) to the innermost value; a + // chained assignment's value is its RHS, all the way down. Not a chain at + // all (the common case) is left untouched: that path is already handled + // correctly below. + const Expr *value = expr->getRHS()->IgnoreParenCasts(); + bool sawChain = false; + while (const auto *nested = dyn_cast(value)) { + if (nested->getOpcode() != BO_Assign) + break; + sawChain = true; + value = nested->getRHS()->IgnoreParenCasts(); + } + if (!sawChain || !isHeapSourcedValue(value)) + return false; + + emitError("assigning a heap-sourced value through a chained assignment is " + "not supported; assign to each variable in a separate statement", + expr->getExprLoc()); + return true; +} + bool SpirvEmitter::isHeapSourcedValue(const Expr *expr) const { if (isDescriptorHeap(expr->IgnoreParenCasts())) return true; diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index cd1e506109..b3a9e4c58f 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -1334,6 +1334,25 @@ class SpirvEmitter : public ASTConsumer { llvm::Optional tryToAssignToDescriptorHeapAlias(const BinaryOperator *assignExpr); + /// \brief Diagnoses `lhs = ;` where lhs is a heap- + /// relevant resource and the chain's innermost value is heap-sourced. + /// isHeapSourcedValue and the alias-propagation below don't recognize a + /// nested assignment as a source, so lhs would otherwise go silently + /// untracked. Returns true if it diagnosed; false otherwise, including for + /// an all-bound-resource chain like `a = b = boundTex;`, which already + /// works. + bool diagnoseChainedDescriptorHeapAssignment(const BinaryOperator *expr); + + /// \brief All descriptor-heap handling for a BO_Assign expression, kept + /// out of doBinaryOperator's dispatch. Evaluates the RHS itself and + /// returns it via *rhs (order matters: a buffer alias must skip normal + /// evaluation; an image alias needs the RHS already evaluated). Returns + /// true if *result is the final value to return as-is; false if the + /// caller should run normal assignment codegen with *rhs. + bool tryHandleDescriptorHeapAssignment(const BinaryOperator *expr, + SpirvInstruction **result, + SpirvInstruction **rhs); + /// \brief Re-derives the buffer-data pointer for a heap buffer alias decl /// (OpLoad of saved index -> OpUntypedAccessChainKHR + OpBufferPointerEXT). /// Returns nullptr if decl is not a recorded alias. diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl index 2bdb84cf87..35db2583df 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.alias-fn-readwrite.hlsl @@ -8,7 +8,8 @@ // // Plain reads/writes work because the loaded handle is enough for spirv-opt to // promote the copies. Atomics fail because the descriptor index does not cross -// OpFunctionCall — see alias-return.hlsl and image-alias-fn-param.hlsl. +// OpFunctionCall — see image-alias-fn-return.error.hlsl and +// image-alias-fn-param.error.hlsl. // Keep this passing when fixing the atomic case. // CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.chained-assignment.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.chained-assignment.error.hlsl new file mode 100644 index 0000000000..7b381b883a --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.chained-assignment.error.hlsl @@ -0,0 +1,44 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=0 %s 2>&1 | FileCheck --check-prefix=IMAGE %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=1 %s 2>&1 | FileCheck --check-prefix=BUFFER %s +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DCASE=2 %s | FileCheck --check-prefix=OK %s + +// Verifies that a chained assignment whose innermost value is heap-sourced, +// e.g. `a = b = ResourceDescriptorHeap[0];`, is diagnosed instead of silently +// skipping alias tracking for `a` and falling back to plain resource-handle +// codegen. The RHS of the outer assignment is itself an assignment +// expression, which isHeapSourcedValue's DeclRefExpr/CallExpr-only +// resolution doesn't recognize, so without this diagnostic `a` never gets a +// heap-index alias and a later atomic on it hits +// VUID-StandaloneSpirv-OpTypeImage-06924 with no error message. +// +// CASE=2 is the control: a chained assignment where every source is a bound +// resource must NOT be rejected. That pattern never touches heap alias +// tracking at all and is already handled correctly by the normal assignment +// path. + +// IMAGE: assigning a heap-sourced value through a chained assignment is not supported +// BUFFER: assigning a heap-sourced value through a chained assignment is not supported +// OK: OpImageWrite + +RWByteAddressBuffer outputBytes : register(u0); +RWTexture2D boundTexA : register(u1); +RWTexture2D boundTexB : register(u2); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { +#if CASE == 0 + RWTexture2D a, b; + a = b = ResourceDescriptorHeap[0]; + uint r; + InterlockedAdd(a[tid.xy], 1, r); + outputBytes.Store(0, r); +#elif CASE == 1 + RWByteAddressBuffer a, b; + a = b = ResourceDescriptorHeap[1]; + outputBytes.Store(0, a.Load(0)); +#elif CASE == 2 + RWTexture2D a, b; + a = b = boundTexA; + a[tid.xy] = b[tid.xy]; +#endif +} From ab9f2fd990a60e4ae8fcb81d3d386a2c559cc4df Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Tue, 29 Sep 2026 11:31:58 -0700 Subject: [PATCH 33/45] fix param heap boundry loss detection order dependence --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 74 ++++++++++++++----- tools/clang/lib/SPIRV/SpirvEmitter.h | 32 ++++---- ...t.image-alias-fn-param-crossing.error.hlsl | 68 +++++++++++++++++ 3 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 1486716b7a..1fd6970f97 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -3497,20 +3497,13 @@ SpirvInstruction *SpirvEmitter::processCall(const CallExpr *callExpr) { return nullptr; } - // A heap image's slot is tracked in the caller's alias state, which the - // callee's parameter doesn't share. Plain reads and writes on the - // parameter still work, since they only need the handle, but an atomic - // needs the slot, so the parameter is flagged for that atomic to report - // the loss. The flag only reaches atomics emitted after this call. - // Heap buffer aliases are rejected below instead: every access through - // one needs the buffer pointer, and passing that pointer would need - // VariablePointersStorageBuffer. - if (spirvOptions.useDescriptorHeap && - !isAKindOfStructuredOrByteBuffer(paramType) && - !isConstantTextureBuffer(paramType) && - isHeapSourcedValue(arg->IgnoreParenCasts())) { - descriptorHeapImageBoundaryLossVars.insert(param); - } + // A heap image's slot lives in the caller's alias state, invisible to + // the callee's parameter; nothing needs to happen here for it. + // isDescriptorHeapImageBoundaryLoss (via paramReceivesHeapSourcedArg) + // diagnoses this later, on demand, only if an atomic on the parameter + // actually needs the slot. Heap buffer aliases are rejected below + // instead: every access through one needs the buffer pointer, so + // there's no atomic-only case to defer. // Any heap-sourced buffer argument (a direct heap subscript, or a // reference to a variable recorded in descriptorHeapBufferAliasVars) @@ -8701,6 +8694,54 @@ bool SpirvEmitter::functionReturnsHeapSourcedImage( return result; } +bool SpirvEmitter::paramReceivesHeapSourcedArg(const ParmVarDecl *param) const { + auto cached = descriptorHeapImageParamLossCache.find(param); + if (cached != descriptorHeapImageParamLossCache.end()) + return cached->second; + // Seed with false before scanning; see functionReturnsHeapSourcedImage for + // why this is a local/second-lookup write rather than live reference. + descriptorHeapImageParamLossCache[param] = false; + + bool result = false; + const auto *owner = dyn_cast_or_null(param->getDeclContext()); + if (owner) { + owner = owner->getCanonicalDecl(); + const unsigned paramIndex = param->getFunctionScopeIndex(); + // No call graph is available, so finding param's callers means walking + // every function body in the TU for a CallExpr targeting owner. + // Shallow scan of TU decls, dont visit methods on user-defined types. + for (const Decl *d : astContext.getTranslationUnitDecl()->decls()) { + const auto *caller = dyn_cast(d); + if (!caller || !caller->getBody()) + continue; + llvm::SmallVector worklist{caller->getBody()}; + while (!worklist.empty()) { + const Stmt *s = worklist.pop_back_val(); + if (!s) + continue; + if (const auto *call = dyn_cast(s)) { + const FunctionDecl *callee = call->getDirectCallee(); + if (callee && callee->getCanonicalDecl() == owner && + paramIndex < call->getNumArgs()) { + llvm::SmallPtrSet visiting; + if (isExprStaticallyHeapSourcedImage(call->getArg(paramIndex), + visiting)) { + result = true; + break; + } + } + } + for (const Stmt *child : s->children()) + worklist.push_back(child); + } + if (result) + break; + } + } + descriptorHeapImageParamLossCache[param] = result; + return result; +} + bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { const Expr *e = expr->IgnoreParenCasts(); @@ -8726,9 +8767,8 @@ bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { const auto *var = dyn_cast_or_null(getReferencedDef(e)); if (!var) return false; - // Parameters are flagged explicitly at their call sites (see processCall). - if (descriptorHeapImageBoundaryLossVars.count(var)) - return true; + if (const auto *param = dyn_cast(var)) + return paramReceivesHeapSourcedArg(param); if (var->getInit() && crossesThroughCall(var->getInit())) return true; return anyAssignmentToVarSatisfies(var, crossesThroughCall); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index b3a9e4c58f..31e4d3c21c 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -33,7 +33,6 @@ #include "clang/SPIRV/FeatureManager.h" #include "clang/SPIRV/SpirvBuilder.h" #include "clang/SPIRV/SpirvContext.h" -#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallPtrSet.h" @@ -309,6 +308,17 @@ class SpirvEmitter : public ASTConsumer { /// rejected at the crossing itself. bool isDescriptorHeapImageBoundaryLoss(const Expr *expr) const; + /// \brief Returns true if any call site in the translation unit passes a + /// statically heap-sourced argument (see isExprStaticallyHeapSourcedImage) + /// for param. + /// + /// Scans the AST rather than recording state at a call site, since + /// work queue can emit param's function before that call site is + /// processed, or the arg can be a local (transitively) sourced from a + /// heap-returning call rather than a runtime-tracked alias variable. + /// Memoized in descriptorHeapImageParamLossCache. + bool paramReceivesHeapSourcedArg(const ParmVarDecl *param) const; + /// \brief Scans var's enclosing function for an assignment `var = rhs;` /// anywhere in the body, calling pred(rhs) for each and returning true on /// the first match. Order/control-flow insensitive: any assignment @@ -1789,27 +1799,15 @@ class SpirvEmitter : public ASTConsumer { llvm::DenseMap descriptorHeapBufferAliasVars; - /// Parameters whose argument, at some call site, was a heap-sourced image - /// (see isHeapSourcedValue): the slot lives in the caller's - /// descriptorHeapImageAliasVars, keyed on the caller's VarDecl, and is - /// invisible to the callee's parameter. Plain OpImageRead/OpImageWrite - /// remain valid on the parameter (the loaded handle alone is sufficient), - /// so this is only consulted lazily, when an atomic needs the heap slot - /// for OpImageTexelPointer. See isDescriptorHeapImageBoundaryLoss. - /// - /// Only parameters are recorded here: a local initialized from a - /// heap-returning call is instead detected on demand by - /// isDescriptorHeapImageBoundaryLoss re-deriving it from the local's own - /// initializer, because whether the call is heap-returning can depend on - /// a callee not yet emitted (see functionReturnsHeapSourcedImage) at the - /// point the local's declaration is processed. - llvm::DenseSet descriptorHeapImageBoundaryLossVars; - /// Memoization cache for functionReturnsHeapSourcedImage, keyed on each /// FunctionDecl's canonical declaration. mutable llvm::DenseMap descriptorHeapImageReturnCache; + /// Memoization cache for paramReceivesHeapSourcedArg. + mutable llvm::DenseMap + descriptorHeapImageParamLossCache; + /// The source location of a push constant block we have previously seen. /// Invalid means no push constant blocks defined thus far. SourceLocation seenPushConstantAt; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl new file mode 100644 index 0000000000..005a9ab54b --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl @@ -0,0 +1,68 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck --check-prefix=ORDER %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_FORWARDED_LOCAL %s 2>&1 | FileCheck --check-prefix=FORWARDED %s + +// Verifies two cases where a heap-sourced argument to a parameter later used +// in an atomic must still be diagnosed, neither of which the old +// processCall-time flag (descriptorHeapImageBoundaryLossVars, set only as a +// side effect of lowering one call) could catch: +// +// Case ORDER - helper() is called with a BOUND image first, which queues and +// fully emits helper() (including its atomic) before the second call site, +// which passes a HEAP image, is ever processed. A flag written during +// that second call arrives too late: helper's body is already emitted. +// Detection must be a static, whole-program scan independent of work-queue +// order (paramReceivesHeapSourcedArg), not a record of what's been lowered +// so far. +// Case FORWARDED_LOCAL - the argument is a local initialized from a call to +// another function that returns a heap-sourced image, not a direct heap +// subscript or an existing alias variable. isHeapSourcedValue (the old +// predicate used at the call site) only consults the runtime alias maps +// and misses this; isExprStaticallyHeapSourcedImage (already used for the +// return-crossing case) recognizes it. + +// ORDER: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// FORWARDED: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported + +RWByteAddressBuffer outputBytes : register(u0); +RWTexture2D bound : register(u1); + +void helper(RWTexture2D t, uint2 coord, out uint orig) { + InterlockedAdd(t[coord], 1, orig); +} + +#ifdef TEST_FORWARDED_LOCAL + +RWTexture2D makeHeapImage(uint slot) { + RWTexture2D tex = ResourceDescriptorHeap[slot]; + return tex; +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture2D local = makeHeapImage(40); + uint r; + helper(local, tid.xy, r); + outputBytes.Store(0, r); +} + +#else + +// Second call site: reached from main only through callWithHeap, which is +// queued after helper (main calls helper(bound) first, so helper is queued +// and then fully emitted at the next work-queue slot, before main's second +// statement -- the call to callWithHeap -- is even processed). +void callWithHeap(uint2 coord, out uint orig) { + RWTexture2D tex = ResourceDescriptorHeap[41]; + helper(tex, coord, orig); +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + uint r0, r1; + helper(bound, tid.xy, r0); + callWithHeap(tid.xy, r1); + outputBytes.Store(0, r0); + outputBytes.Store(4, r1); +} + +#endif From 3061e45c3dd2d36cef51156d514f36d89b271a4f Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Tue, 29 Sep 2026 13:11:47 -0700 Subject: [PATCH 34/45] diagnose interlocked ops on globals and static locals --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 37 +++++++++++----- ...heap.ext.image-alias-file-scope.error.hlsl | 42 +++++++++++++++++++ ...t.image-alias-fn-param-crossing.error.hlsl | 4 +- ...orheap.ext.image-alias-fn-param.error.hlsl | 2 +- ...t.image-alias-fn-return-forward.error.hlsl | 6 +-- ...rheap.ext.image-alias-fn-return.error.hlsl | 2 +- 6 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-file-scope.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 1fd6970f97..11bfbbf444 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -8603,7 +8603,11 @@ bool SpirvEmitter::isHeapSourcedValue(const Expr *expr) const { bool SpirvEmitter::isExprStaticallyHeapSourcedImage( const Expr *expr, llvm::SmallPtrSetImpl &visiting) const { - expr = expr->IgnoreParenCasts(); + // A static local's initializer is wrapped in an ExprWithCleanups (for its + // exactly-once-init semantics) that a plain local's or a true global's is + // not; IgnoreImplicit sees through that (and MaterializeTemporaryExpr / + // CXXBindTemporaryExpr) before IgnoreParenCasts strips the rest. + expr = expr->IgnoreImplicit()->IgnoreParenCasts(); if (isDescriptorHeap(expr)) return true; // expr may itself be a call to another function that returns a @@ -8769,6 +8773,14 @@ bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { return false; if (const auto *param = dyn_cast(var)) return paramReceivesHeapSourcedArg(param); + // A global or static-local var is never tracked: doVarDecl only calls + // tryToAssignDescriptorHeapImageAlias for ordinary function-local + // variables, so descriptorHeapImageAliasVars never gets an entry for one, + // even when it's heap-sourced directly (no call boundary crossed at all). + if (!var->hasLocalStorage()) { + llvm::SmallPtrSet visiting; + return isExprStaticallyHeapSourcedImage(e, visiting); + } if (var->getInit() && crossesThroughCall(var->getInit())) return true; return anyAssignmentToVarSatisfies(var, crossesThroughCall); @@ -11368,21 +11380,24 @@ SpirvEmitter::processIntrinsicInterlockedMethod(const CallExpr *expr, auto *coordInstr = doExpr(index); // An atomic on a heap image needs its heap slot, which doesn't survive - // a function call. Without this check, the fallback below emits - // OpImageTexelPointer on a Function-class copy, which fails - // VUID-StandaloneSpirv-OpTypeImage-06924 with no diagnostic. The loss - // is traced to the crossing rather than inferred from Function storage - // class, because bound images copied into locals or parameters are - // Function-class too and legalize fine. Detection is best-effort. + // a function call, and is never recorded at all for a global or + // static-local variable. Without this check, the fallback below emits + // OpImageTexelPointer on a Private- or Function-class copy, which + // fails VUID-StandaloneSpirv-OpTypeImage-06924 with no diagnostic. The + // loss is traced to these specific causes rather than inferred from + // storage class, because bound images copied into locals or + // parameters are Function-class too and legalize fine. Detection is + // best-effort. // TODO(#8784): implement cross-function heap image propagation; once - // the slot itself can cross the boundary, remove this check and it's + // the slot itself can cross the boundary, remove this check and its // tracker if (spirvOptions.useDescriptorHeap && isDescriptorHeapImageBoundaryLoss(base)) { emitError( - "interlocked operation on a heap-backed RWTexture passed to or " - "returned from a helper function is not supported; use the " - "image directly at the call site", + "interlocked operation on a heap-backed RWTexture must read the " + "descriptor directly at this call site; it cannot be passed " + "through a function call, returned from one, or declared as a " + "global or static-local variable", dest->getExprLoc()); return nullptr; } diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-file-scope.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-file-scope.error.hlsl new file mode 100644 index 0000000000..8221ad526e --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-file-scope.error.hlsl @@ -0,0 +1,42 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck --check-prefix=GLOBAL %s +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv -DTEST_STATIC_LOCAL %s 2>&1 | FileCheck --check-prefix=STATIC_LOCAL %s + +// Verifies: an atomic on a file-scope (static global) or static-local +// RWTexture initialized from ResourceDescriptorHeap is rejected, instead of +// silently falling through to an invalid OpImageTexelPointer on a +// Private-class variable (VUID-StandaloneSpirv-OpTypeImage-06924, no +// diagnostic). +// +// doVarDecl only calls tryToAssignDescriptorHeapImageAlias / +// tryToCreateDescriptorHeapAlias in the local-storage branch; neither ever +// runs for a file-scope declaration, so its heap index is never tracked. A +// plain (non-atomic) heap-sourced file-scope RWTexture/StructuredBuffer is +// valid and unaffected -- see static-global.hlsl. + +// GLOBAL: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site +// STATIC_LOCAL: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site + +RWByteAddressBuffer outputBytes : register(u0); + +#ifdef TEST_STATIC_LOCAL + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + static RWTexture2D g = ResourceDescriptorHeap[0]; + uint orig; + InterlockedAdd(g[tid.xy], 1, orig); + outputBytes.Store(0, orig); +} + +#else + +static RWTexture2D g = ResourceDescriptorHeap[0]; + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + uint orig; + InterlockedAdd(g[tid.xy], 1, orig); + outputBytes.Store(0, orig); +} + +#endif diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl index 005a9ab54b..8878c2ec75 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-crossing.error.hlsl @@ -20,8 +20,8 @@ // and misses this; isExprStaticallyHeapSourcedImage (already used for the // return-crossing case) recognizes it. -// ORDER: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported -// FORWARDED: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// ORDER: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site +// FORWARDED: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site RWByteAddressBuffer outputBytes : register(u0); RWTexture2D bound : register(u1); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl index 8d108b23fb..746d7d7b3c 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param.error.hlsl @@ -16,7 +16,7 @@ // TODO(#8784): accept once the descriptor index is propagated across function // boundaries. -// CHECK: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// CHECK: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site RWByteAddressBuffer outputBytes : register(u0); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl index 7d2d25fa2e..595f45bf49 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return-forward.error.hlsl @@ -21,9 +21,9 @@ // is NOT a loss (see sm6_6.descriptorheap.ext.rwtexture-atomics.hlsl's // reassignment case). -// FORWARD: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported -// REASSIGN: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported -// LOCAL: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// FORWARD: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site +// REASSIGN: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site +// LOCAL: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site RWByteAddressBuffer outputBytes : register(u0); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl index e8a4023176..366358e944 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-return.error.hlsl @@ -23,7 +23,7 @@ // TODO(#8784): accept once the descriptor index is propagated across function // boundaries. -// CHECK: interlocked operation on a heap-backed RWTexture passed to or returned from a helper function is not supported +// CHECK: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site RWByteAddressBuffer outputBytes : register(u0); From 5261c0b340ecf350a2207cd4b78411755973012b Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Tue, 29 Sep 2026 14:55:30 -0700 Subject: [PATCH 35/45] drop implementation details from API documentation --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 44 +++++++++------------- tools/clang/lib/SPIRV/SpirvEmitter.h | 52 ++++---------------------- 2 files changed, 26 insertions(+), 70 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 11bfbbf444..fd2bc86e99 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -3233,12 +3233,9 @@ SpirvEmitter::tryToAssignToDescriptorHeapAlias( } /// \brief All descriptor-heap handling for a BO_Assign expression. -/// Evaluates the RHS itself and returns it via *rhs, since order matters here: -/// image-alias tracking below needs the RHS already evaluated (reads side -/// effect of it), while a buffer alias must skip normal evaluation entirely. -/// Returns true if *result is the assignment's final value, to be returned -/// as-is; false if the caller should run normal assignment codegen with -/// *rhs. +/// Evaluates the RHS itself and returns it via *rhs. Returns true if +/// *result is the assignment's final value, to be returned as-is; false +/// if the caller should run normal assignment codegen with *rhs. bool SpirvEmitter::tryHandleDescriptorHeapAssignment(const BinaryOperator *expr, SpirvInstruction **result, SpirvInstruction **rhs) { @@ -8603,10 +8600,9 @@ bool SpirvEmitter::isHeapSourcedValue(const Expr *expr) const { bool SpirvEmitter::isExprStaticallyHeapSourcedImage( const Expr *expr, llvm::SmallPtrSetImpl &visiting) const { - // A static local's initializer is wrapped in an ExprWithCleanups (for its - // exactly-once-init semantics) that a plain local's or a true global's is - // not; IgnoreImplicit sees through that (and MaterializeTemporaryExpr / - // CXXBindTemporaryExpr) before IgnoreParenCasts strips the rest. + // IgnoreImplicit strips ExprWithCleanups (added for a static local's + // exactly-once-init), MaterializeTemporaryExpr, and CXXBindTemporaryExpr, + // which IgnoreParenCasts alone leaves in place. expr = expr->IgnoreImplicit()->IgnoreParenCasts(); if (isDescriptorHeap(expr)) return true; @@ -8773,10 +8769,9 @@ bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { return false; if (const auto *param = dyn_cast(var)) return paramReceivesHeapSourcedArg(param); - // A global or static-local var is never tracked: doVarDecl only calls - // tryToAssignDescriptorHeapImageAlias for ordinary function-local - // variables, so descriptorHeapImageAliasVars never gets an entry for one, - // even when it's heap-sourced directly (no call boundary crossed at all). + // doVarDecl only tracks ordinary function-local vars, so a global or + // static-local never gets a descriptorHeapImageAliasVars entry, even when + // heap-sourced directly (no call boundary crossed). if (!var->hasLocalStorage()) { llvm::SmallPtrSet visiting; return isExprStaticallyHeapSourcedImage(e, visiting); @@ -11379,18 +11374,15 @@ SpirvEmitter::processIntrinsicInterlockedMethod(const CallExpr *expr, } auto *coordInstr = doExpr(index); - // An atomic on a heap image needs its heap slot, which doesn't survive - // a function call, and is never recorded at all for a global or - // static-local variable. Without this check, the fallback below emits - // OpImageTexelPointer on a Private- or Function-class copy, which - // fails VUID-StandaloneSpirv-OpTypeImage-06924 with no diagnostic. The - // loss is traced to these specific causes rather than inferred from - // storage class, because bound images copied into locals or - // parameters are Function-class too and legalize fine. Detection is - // best-effort. - // TODO(#8784): implement cross-function heap image propagation; once - // the slot itself can cross the boundary, remove this check and its - // tracker + // An atomic on a heap image needs its heap slot. That slot doesn't + // survive a function call, and is never recorded at all for a global + // or static-local var. Without this check, the fallback below emits + // OpImageTexelPointer on a Private- or Function-class copy, failing + // VUID-StandaloneSpirv-OpTypeImage-06924 with no diagnostic. Traced to + // these specific causes, not storage class, since bound images in + // locals/params are Function-class too and legalize fine. + // TODO(#8784): cross-function heap image propagation; once the slot + // can cross the boundary, remove this check and its tracker. if (spirvOptions.useDescriptorHeap && isDescriptorHeapImageBoundaryLoss(base)) { emitError( diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 31e4d3c21c..e73c884d84 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -297,42 +297,21 @@ class SpirvEmitter : public ASTConsumer { /// \brief Returns true if expr is a heap image whose heap slot was lost /// crossing a user function call, as a parameter or a return value. - /// - /// The slot is recorded per variable in the function that indexed the heap - /// and isn't propagated across calls, so after a crossing only the image - /// handle remains. A value sourced directly from a heap subscript within - /// the current function, with no call involved, is NOT a loss: no - /// boundary was crossed, so descriptorHeapImageAliasVars (populated by - /// that same-function declaration or assignment) already has what's - /// needed. expr's type isn't checked, since heap buffer aliases are - /// rejected at the crossing itself. bool isDescriptorHeapImageBoundaryLoss(const Expr *expr) const; /// \brief Returns true if any call site in the translation unit passes a /// statically heap-sourced argument (see isExprStaticallyHeapSourcedImage) /// for param. - /// - /// Scans the AST rather than recording state at a call site, since - /// work queue can emit param's function before that call site is - /// processed, or the arg can be a local (transitively) sourced from a - /// heap-returning call rather than a runtime-tracked alias variable. - /// Memoized in descriptorHeapImageParamLossCache. bool paramReceivesHeapSourcedArg(const ParmVarDecl *param) const; /// \brief Scans var's enclosing function for an assignment `var = rhs;` /// anywhere in the body, calling pred(rhs) for each and returning true on - /// the first match. Order/control-flow insensitive: any assignment - /// anywhere in the function counts, not only ones that reach a particular - /// use. + /// the first match. bool anyAssignmentToVarSatisfies( const VarDecl *var, llvm::function_ref pred) const; /// \brief Returns true if a return statement in fn yields a statically /// heap-sourced value (see isExprStaticallyHeapSourcedImage). - /// - /// Scans the AST rather than recording state while emitting fn, because - /// the work queue can emit fn after the caller that needs the answer. - /// Memoized in descriptorHeapImageReturnCache. bool functionReturnsHeapSourcedImage(const FunctionDecl *fn) const; /// \brief Returns true if expr is a descriptor heap subscript, a call to a @@ -343,11 +322,7 @@ class SpirvEmitter : public ASTConsumer { /// RWTexture2D b; /// b = a; // b /// - /// Reads initializers and assignments from the AST rather than - /// descriptorHeapImageAliasVars, so it can answer for a function that - /// hasn't been emitted yet. Assignment search is order/control-flow - /// insensitive (any assignment to the VarDecl anywhere in its function - /// counts). `visiting` guards against revisiting a VarDecl. + /// visiting tracks VarDecls already seen on the current call stack. bool isExprStaticallyHeapSourcedImage( const Expr *expr, llvm::SmallPtrSetImpl &visiting) const; @@ -1307,14 +1282,7 @@ class SpirvEmitter : public ASTConsumer { /// \brief Diagnoses a local resource variable assigned from both a bound /// resource and ResourceDescriptorHeap. /// - /// Once recorded as an alias, every use is re-lowered as a heap access - /// chain regardless of control flow, correct only if the variable holds - /// a heap descriptor on every reaching path. Mixed sources violate this; - /// reject rather than silently miscompile. - /// - /// Returns true if rejected (new diagnostic emitted or variable already - /// diagnosed); callers need not act, alias-recording helpers check - /// descriptorHeapVarState and skip rejected variables automatically. + /// Issues a diagnostic and returns true if rejected. bool diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, const Expr *srcExpr, SourceLocation loc); @@ -1346,19 +1314,15 @@ class SpirvEmitter : public ASTConsumer { /// \brief Diagnoses `lhs = ;` where lhs is a heap- /// relevant resource and the chain's innermost value is heap-sourced. - /// isHeapSourcedValue and the alias-propagation below don't recognize a - /// nested assignment as a source, so lhs would otherwise go silently - /// untracked. Returns true if it diagnosed; false otherwise, including for - /// an all-bound-resource chain like `a = b = boundTex;`, which already - /// works. + /// Returns true if it diagnosed; false otherwise, including for an + /// all-bound-resource chain like `a = b = boundTex;`, which already works. bool diagnoseChainedDescriptorHeapAssignment(const BinaryOperator *expr); /// \brief All descriptor-heap handling for a BO_Assign expression, kept /// out of doBinaryOperator's dispatch. Evaluates the RHS itself and - /// returns it via *rhs (order matters: a buffer alias must skip normal - /// evaluation; an image alias needs the RHS already evaluated). Returns - /// true if *result is the final value to return as-is; false if the - /// caller should run normal assignment codegen with *rhs. + /// returns it via *rhs. Returns true if *result is the final value to + /// return as-is; false if the caller should run normal assignment codegen + /// with *rhs. bool tryHandleDescriptorHeapAssignment(const BinaryOperator *expr, SpirvInstruction **result, SpirvInstruction **rhs); From fc9380c29779df0fe92924b01605f333c774b6af Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Tue, 29 Sep 2026 16:11:08 -0700 Subject: [PATCH 36/45] fix heap image detection for chained func params --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 8 +++++ ...xt.image-alias-fn-param-forward.error.hlsl | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-forward.error.hlsl diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index fd2bc86e99..c617a34fac 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -8618,6 +8618,14 @@ bool SpirvEmitter::isExprStaticallyHeapSourcedImage( declRefExpr ? dyn_cast(declRefExpr->getDecl()) : nullptr; if (!var || !visiting.insert(var).second) return false; + // var may itself be a parameter that forwards a heap-sourced argument from + // its caller(s), ie: forward(RWTexture2D t) { atomicHelper(t); } + // called with a heap image. A param has no init/ assignment of its own to + // find below; only paramReceivesHeapSourcedArg can see the value it was + // called with. + if (const auto *param = dyn_cast(var)) + if (paramReceivesHeapSourcedArg(param)) + return true; if (var->getInit() && isExprStaticallyHeapSourcedImage(var->getInit(), visiting)) return true; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-forward.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-forward.error.hlsl new file mode 100644 index 0000000000..18e741ba94 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.image-alias-fn-param-forward.error.hlsl @@ -0,0 +1,33 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// Verifies a heap-sourced image forwarded through TWO levels of parameters +// is still diagnosed: +// main() calls forward(heapImage), and forward's own parameter `t` (which +// has no initializer or local assignment of its own -- it only ever +// receives its value as a call argument) is passed on to atomicHelper, +// which uses its parameter in an atomic. +// +// isExprStaticallyHeapSourcedImage's VarDecl handling must recurse into +// paramReceivesHeapSourcedArg when the var is itself a ParmVarDecl, not just +// check its (nonexistent) initializer/assignment, or this chain silently +// falls through to an invalid OpImageTexelPointer. + +// CHECK: interlocked operation on a heap-backed RWTexture must read the descriptor directly at this call site + +RWByteAddressBuffer outputBytes : register(u0); + +void atomicHelper(RWTexture2D u, uint2 coord, out uint orig) { + InterlockedAdd(u[coord], 1, orig); +} + +void forward(RWTexture2D t, uint2 coord, out uint orig) { + atomicHelper(t, coord, orig); +} + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RWTexture2D tex = ResourceDescriptorHeap[40]; + uint r; + forward(tex, tid.xy, r); + outputBytes.Store(0, r); +} From 96cdb4efd5aabba75b39db268be5c0de1582f1fe Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 5 Oct 2026 10:28:59 -0700 Subject: [PATCH 37/45] fix some autos --- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index c617a34fac..7add4fc8d7 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -8609,7 +8609,7 @@ bool SpirvEmitter::isExprStaticallyHeapSourcedImage( // expr may itself be a call to another function that returns a // heap-sourced image, e.g. `return makeHeapImage();`. if (const auto *call = dyn_cast(expr)) { - if (const auto *fn = call->getDirectCallee()) + if (const FunctionDecl *fn = call->getDirectCallee()) return functionReturnsHeapSourcedImage(fn); return false; } @@ -8765,7 +8765,7 @@ bool SpirvEmitter::isDescriptorHeapImageBoundaryLoss(const Expr *expr) const { // declaration or assignment, already has what's needed. auto crossesThroughCall = [this](const Expr *x) { if (const auto *call = dyn_cast(x->IgnoreParenCasts())) - if (const auto *fn = call->getDirectCallee()) + if (const FunctionDecl *fn = call->getDirectCallee()) return functionReturnsHeapSourcedImage(fn); return false; }; From bef4d207e145642c0ff9fc8c6a0b4fba5263cb4e Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 5 Oct 2026 12:50:06 -0700 Subject: [PATCH 38/45] clean up comments --- tools/clang/include/clang/SPIRV/SpirvBuilder.h | 7 +++---- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 5 +++++ tools/clang/lib/SPIRV/SpirvEmitter.h | 12 +++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index afd8f42849..6eeddfd533 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -834,7 +834,7 @@ class SpirvBuilder { /// \brief Returns the OpConstantSizeOfEXT (SPV_EXT_descriptor_heap) for /// descriptor operandType: client-API size in bytes as a 32-bit unsigned - /// integer. Cached per operand type. + /// integer. SpirvConstant *getConstantSizeOfEXT(const SpirvType *operandType); SpirvSpecConstantTernaryOp * @@ -843,12 +843,11 @@ class SpirvBuilder { SpirvInstruction *op3, SourceLocation loc); /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays: - /// max(sizeof(image), sizeof(buffer)) computed via OpSpecConstantOp. - /// Cached per module. + /// max(sizeof(image), sizeof(buffer)). SpirvInstruction *getResourceHeapArrayStride(); /// \brief Shared ArrayStrideIdEXT operand for sampler-heap runtime arrays: - /// the sampler descriptor size. Cached per module. + /// the sampler descriptor size. SpirvInstruction *getSamplerHeapArrayStride(); SpirvUndef *getUndef(QualType); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 7add4fc8d7..659905f270 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -5240,6 +5240,8 @@ void SpirvEmitter::markDescriptorHeapCounterUnsupported( } bool SpirvEmitter::isDescriptorHeapCounterUnsupported(const Expr *expr) const { + // Consults the counterUnsupported field of the buffer alias entry for the + // referenced variable. if (const auto *var = dyn_cast_or_null(getReferencedDef(expr))) { auto it = descriptorHeapBufferAliasVars.find(var); if (it != descriptorHeapBufferAliasVars.end()) @@ -5449,6 +5451,8 @@ bool SpirvEmitter::tryToAssignDescriptorHeapBufferAlias(const Expr *dstExpr, SpirvInstruction * SpirvEmitter::emitDescriptorHeapBufferPointer(const VarDecl *decl, SourceLocation loc) { + // Lowering: OpLoad of saved index -> OpUntypedAccessChainKHR + + // OpBufferPointerEXT. auto found = descriptorHeapBufferAliasVars.find(decl); if (found == descriptorHeapBufferAliasVars.end()) return nullptr; @@ -5496,6 +5500,7 @@ getDescriptorHeapBufferStorageClass(QualType resourceType) { SpirvInstruction *SpirvEmitter::emitDescriptorHeapBufferAccess( QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, const Expr *expr, const Expr *baseExpr, const Expr *indexExpr) { + // Lowering: OpUntypedAccessChainKHR -> OpBufferPointerEXT. const UntypedPointerKHRType *untypedUniformConstantType = spvContext.getUntypedPointerKHRType(spv::StorageClass::UniformConstant); LowerTypeVisitor lowerTypeVisitor(astContext, spvContext, spirvOptions, diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index e73c884d84..3c83ef7760 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -446,9 +446,9 @@ class SpirvEmitter : public ASTConsumer { /// Emits the descriptor-heap buffer access for a buffer-like resource /// (StructuredBuffer/ByteAddressBuffer/ConstantBuffer/TextureBuffer + RW - /// variants) loaded from heapVar at index via OpUntypedAccessChainKHR -> - /// OpBufferPointerEXT. Records in descriptorHeapBufferAccesses[expr], - /// returning the buffer-data pointer or nullptr on lowering failure. + /// variants) loaded from heapVar at index. Records the access in + /// descriptorHeapBufferAccesses[expr], returning the buffer-data pointer + /// or nullptr on lowering failure. // Caller must have checked resource is buffer-like. SpirvInstruction *emitDescriptorHeapBufferAccess( QualType resourceType, SpirvInstruction *heapVar, SpirvInstruction *index, @@ -1263,8 +1263,7 @@ class SpirvEmitter : public ASTConsumer { /// \brief Returns true if counter operations on the resource expression are /// known to be unsupported because the resource came from - /// ResourceDescriptorHeap. Consults the counterUnsupported field of the - /// buffer alias entry for the referenced variable. + /// ResourceDescriptorHeap. bool isDescriptorHeapCounterUnsupported(const Expr *expr) const; /// \brief Records the heap index for a local image alias when the source @@ -1327,8 +1326,7 @@ class SpirvEmitter : public ASTConsumer { SpirvInstruction **result, SpirvInstruction **rhs); - /// \brief Re-derives the buffer-data pointer for a heap buffer alias decl - /// (OpLoad of saved index -> OpUntypedAccessChainKHR + OpBufferPointerEXT). + /// \brief Re-derives the buffer-data pointer for a heap buffer alias decl. /// Returns nullptr if decl is not a recorded alias. SpirvInstruction *emitDescriptorHeapBufferPointer(const VarDecl *decl, SourceLocation loc); From 58538da36e23e337dee74150e6ae28d34b807617 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 11 Sep 2026 15:06:10 -0700 Subject: [PATCH 39/45] corrected release notes --- docs/ReleaseNotes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1f1e20d7bb..cc65e3752a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -24,7 +24,8 @@ The included licenses apply to the following files: - Extended `SPV_EXT_descriptor_heap` lowering (`-fspv-use-descriptor-heap`) to buffer-like resources (`StructuredBuffer`, `ByteAddressBuffer`, `ConstantBuffer`, `TextureBuffer`, and RW variants) via - `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3`. + `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3` + [#8517](https://github.com/microsoft/DirectXShaderCompiler/pull/8517). Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. From 70f989961a3eb229a6d9ec20a0406b0daecefd9c Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Thu, 4 Jun 2026 13:19:50 -0700 Subject: [PATCH 40/45] [SPIR-V] Add descriptor heap RaytracingAccelerationStructure support Extends the SPV_EXT_descriptor_heap native heap lowering to cover RaytracingAccelerationStructure resources loaded from ResourceDescriptorHeap. Acceleration structure descriptors are accessed via OpUntypedAccessChainKHR into a runtime array of OpTypeAccelerationStructureKHR, consistent with the image and sampler paths added in the previous commit. --- .../clang/include/clang/SPIRV/AstTypeProbe.h | 4 ++ tools/clang/lib/SPIRV/AstTypeProbe.cpp | 7 ++++ tools/clang/lib/SPIRV/SpirvEmitter.cpp | 10 +++++ ...riptorheap.ext.acceleration-structure.hlsl | 42 +++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl diff --git a/tools/clang/include/clang/SPIRV/AstTypeProbe.h b/tools/clang/include/clang/SPIRV/AstTypeProbe.h index 214df82d45..207d02a3b2 100644 --- a/tools/clang/include/clang/SPIRV/AstTypeProbe.h +++ b/tools/clang/include/clang/SPIRV/AstTypeProbe.h @@ -250,6 +250,10 @@ bool isBuffer(QualType type); /// \brief Returns true if the given type is the HLSL RWBuffer type. bool isRWBuffer(QualType type); +/// \brief Returns true if the given type is the HLSL +/// RaytracingAccelerationStructure type. +bool isRaytracingAccelerationStructure(QualType type); + /// \brief Returns true if the given type is an HLSL Texture type. bool isTexture(QualType); diff --git a/tools/clang/lib/SPIRV/AstTypeProbe.cpp b/tools/clang/lib/SPIRV/AstTypeProbe.cpp index a871bd3577..8c766445a3 100644 --- a/tools/clang/lib/SPIRV/AstTypeProbe.cpp +++ b/tools/clang/lib/SPIRV/AstTypeProbe.cpp @@ -917,6 +917,13 @@ bool isBuffer(QualType type) { return false; } +bool isRaytracingAccelerationStructure(QualType type) { + if (const auto *rt = type->getAs()) { + return rt->getDecl()->getName() == "RaytracingAccelerationStructure"; + } + return false; +} + bool isRWTexture(QualType type) { if (const auto *rt = type->getAs()) { const auto name = rt->getDecl()->getName(); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 659905f270..e4880dc483 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2034,6 +2034,16 @@ bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, return tryToAssignDescriptorHeapBufferAlias(decl, init); } + if (isRaytracingAccelerationStructure(decl->getType())) { + if (auto *initVal = loadIfGLValue(init)) + declIdMapper.registerFnVarAlias(decl, initVal); + else + emitError("cannot create descriptor heap acceleration structure alias " + "from initializer", + init->getExprLoc()); + return true; + } + return false; } diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl new file mode 100644 index 0000000000..7c5c9fdcb9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl @@ -0,0 +1,42 @@ +// RUN: %dxc -T lib_6_6 -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -fvk-resource-heap-stride 64 -fvk-sampler-heap-stride 32 -fspv-extension=SPV_KHR_ray_tracing -fspv-extension=SPV_EXT_descriptor_heap -fspv-extension=SPV_KHR_untyped_pointers -spirv %s | FileCheck %s + +// Verifies: ResourceDescriptorHeap of a RaytracingAccelerationStructure +// lowers to an acceleration-structure runtime array, loads the accel +// handle from the heap, and emits OpTraceRayKHR under the ray-tracing +// capability/extension. + +// CHECK: OpCapability RayTracingKHR +// CHECK: OpExtension "SPV_KHR_ray_tracing" + +// CHECK-DAG: %[[UntypedUniformConstant:[a-zA-Z0-9_]+]] = OpTypeUntypedPointerKHR UniformConstant +// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR +// CHECK-DAG: %[[ASArray:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] +// CHECK: %[[ResourceHeap:[a-zA-Z0-9_]+]] = OpUntypedVariableKHR %[[UntypedUniformConstant]] UniformConstant + +struct Payload { + float4 color; +}; + +struct Attribute { + float2 bary; +}; + +[shader("closesthit")] +void main(inout Payload payload, in Attribute attr) { + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[3]; + + RayDesc ray; + ray.Origin = float3(0.0f, 0.0f, 0.0f); + ray.Direction = float3(0.0f, 0.0f, -1.0f); + ray.TMin = 0.0f; + ray.TMax = 1000.0f; + + Payload childPayload = { float4(attr.bary, 0.0f, 1.0f) }; + + // CHECK: %[[Desc:[a-zA-Z0-9_]+]] = OpUntypedAccessChainKHR %[[UntypedUniformConstant]] %[[ASArray]] %[[ResourceHeap]] %uint_3 + // CHECK: %[[Scene:[a-zA-Z0-9_]+]] = OpLoad %[[Accel]] %[[Desc]] + // CHECK: OpTraceRayKHR %[[Scene]] + TraceRay(scene, 0x0, 0xff, 0, 1, 0, ray, childPayload); + + payload.color = childPayload.color; +} From 4235185434082b031e53c4cc63ec2a67bfe9eb41 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 20 Jul 2026 15:40:40 -0700 Subject: [PATCH 41/45] removed erronous CLI options from test --- .../sm6_6.descriptorheap.ext.acceleration-structure.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl index 7c5c9fdcb9..57ba2c3d68 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.hlsl @@ -1,4 +1,4 @@ -// RUN: %dxc -T lib_6_6 -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -fvk-resource-heap-stride 64 -fvk-sampler-heap-stride 32 -fspv-extension=SPV_KHR_ray_tracing -fspv-extension=SPV_EXT_descriptor_heap -fspv-extension=SPV_KHR_untyped_pointers -spirv %s | FileCheck %s +// RUN: %dxc -T lib_6_6 -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -fspv-extension=SPV_KHR_ray_tracing -fspv-extension=SPV_EXT_descriptor_heap -fspv-extension=SPV_KHR_untyped_pointers -spirv %s | FileCheck %s // Verifies: ResourceDescriptorHeap of a RaytracingAccelerationStructure // lowers to an acceleration-structure runtime array, loads the accel From 71fa060d9e70ac4a447d20c6f1bde19373cb90be Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Wed, 22 Jul 2026 14:12:52 -0700 Subject: [PATCH 42/45] Included AccelerationStructure in resource heap stride calculation --- .../clang/include/clang/SPIRV/SpirvBuilder.h | 29 +++- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 41 +++-- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 103 +++++++++++- ...t.acceleration-structure.stride.error.hlsl | 46 ++++++ ...ptorheap.ext.array-stride.accelstruct.hlsl | 154 ++++++++++++++++++ 5 files changed, 354 insertions(+), 19 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl create mode 100644 tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 6eeddfd533..2e6b49bd17 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -842,8 +842,27 @@ class SpirvBuilder { SpirvInstruction *op1, SpirvInstruction *op2, SpirvInstruction *op3, SourceLocation loc); - /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays: - /// max(sizeof(image), sizeof(buffer)). + /// \brief Record that acceleration structures may occupy the resource heap. + /// Note: Must be called before getResourceHeapArrayStride() (before the + /// code-gen loop in HandleTranslationUnit) so the cached stride is correct + /// on the first call. Calling it later has no effect because the result is + /// frozen after the first getResourceHeapArrayStride() invocation. + void noteResourceHeapHasAccelStruct() { resourceHeapHasAccelStruct = true; } + + /// \brief Returns whether the resource-heap stride accounts for acceleration + /// structure descriptors. Code-gen must reject an acceleration structure heap + /// access when this is false: the decision is made before the code-gen loop + /// and the stride cannot be widened afterwards. + bool resourceHeapStrideIncludesAccelStruct() const { + return resourceHeapHasAccelStruct; + } + + /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays. + /// Default: max(sizeof(image), sizeof(buffer)) + /// With RT: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) + /// Computed via OpSpecConstantOp and cached per module. + /// Note: noteResourceHeapHasAccelStruct() must be called before this if AS + /// may be present (result is frozen on the first call). SpirvInstruction *getResourceHeapArrayStride(); /// \brief Shared ArrayStrideIdEXT operand for sampler-heap runtime arrays: @@ -974,6 +993,12 @@ class SpirvBuilder { SpirvInstruction *resourceHeapArrayStride = nullptr; SpirvInstruction *samplerHeapArrayStride = nullptr; + /// Set by noteResourceHeapHasAccelStruct() when HandleTranslationUnit + /// detects that the shader uses ray-tracing features. When true, + /// getResourceHeapArrayStride() extends the stride to include + /// sizeof(acceleration_structure). + bool resourceHeapHasAccelStruct = false; + SpirvDebugInfoNone *debugNone; /// DebugExpression that does not reference any DebugOperation diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index e8fa6c097e..3e16ee46de 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2052,16 +2052,24 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { if (resourceHeapArrayStride) return resourceHeapArrayStride; - // ResourceDescriptorHeap is a flat array: any descriptor may sit at any slot. - // DX12 semantics require all resource arrays share stride = - // max(sizeof(image), sizeof(buffer)). + // ResourceDescriptorHeap is a flat array; any descriptor may sit at any slot. + // DX12 semantics require all resource arrays share one stride: + // stride = max(sizeof(image), sizeof(buffer) [, sizeof(accel_struct)]) + // + // Categories and placeholders: + // image: - always included; textures lower to OpTypeImage, so this covers + // all non-RT resource kinds. Representative: canonical sampled + // 2D float image (subtype/storage class do not affect size). + // buffer - always included. Representative: Uniform buffer. + // accel - included only when noteResourceHeapHasAccelStruct() was called. + // Placeholder: getAccelerationStructureType(). Requires + // OpCapability RayTracingKHR, guaranteed present when + // noteResourceHeapHasAccelStruct() is called. + // // VkPhysicalDeviceDescriptorHeapPropertiesEXT reports one size per category - // (imageDescriptorSize / bufferDescriptorSize); textures lower to - // OpTypeImage, so image/buffer covers all relevant HLSL resource kinds. Sizes - // are driver-defined (known only at pipeline creation), so the max is - // computed via OpSpecConstantOp over two OpConstantSizeOfEXT placeholders. - // A canonical sampled 2D float image and a Uniform buffer serve as - // representatives; subtype and storage class do not affect the category size. + // (imageDescriptorSize / bufferDescriptorSize); all sizes are driver-defined. + // The max is computed via OpSpecConstantOp over OpConstantSizeOfEXT + // placeholders: 2 for non-RT shaders, 3 when accel struct is present. const SpirvType *placeholderImage = context.getImageType( context.getFloatType(32), spv::Dim::Dim2D, ImageType::WithDepth::No, /*arrayed*/ false, /*ms*/ false, ImageType::WithSampler::Yes, @@ -2073,9 +2081,22 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { SpirvInstruction *bufferSize = getConstantSizeOfEXT(placeholderBuffer); SpirvInstruction *imageIsBigger = createSpecConstantBinaryOp( spv::Op::OpUGreaterThan, astContext.BoolTy, imageSize, bufferSize, {}); - resourceHeapArrayStride = + SpirvInstruction *maxImgBuf = createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, imageIsBigger, imageSize, bufferSize, {}); + + if (resourceHeapHasAccelStruct) { + // Extend to max(max(img, buf), accel_struct). + const SpirvType *placeholderAS = context.getAccelerationStructureTypeNV(); + SpirvInstruction *asSize = getConstantSizeOfEXT(placeholderAS); + SpirvInstruction *maxImgBufIsBigger = createSpecConstantBinaryOp( + spv::Op::OpUGreaterThan, astContext.BoolTy, maxImgBuf, asSize, {}); + resourceHeapArrayStride = + createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, + maxImgBufIsBigger, maxImgBuf, asSize, {}); + } else { + resourceHeapArrayStride = maxImgBuf; + } return resourceHeapArrayStride; } diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index e4880dc483..816a32abb1 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -67,6 +67,23 @@ bool isImplicitVarDeclInVkNamespace(const Decl *decl) { return false; } +// Returns true if the given shader model kind is a ray-tracing stage. +// Used to detect whether the resource heap stride must include +// sizeof(acceleration_structure). +bool shaderModelKindIsRayTracing(hlsl::ShaderModel::Kind k) { + switch (k) { + case hlsl::ShaderModel::Kind::RayGeneration: + case hlsl::ShaderModel::Kind::Intersection: + case hlsl::ShaderModel::Kind::AnyHit: + case hlsl::ShaderModel::Kind::ClosestHit: + case hlsl::ShaderModel::Kind::Miss: + case hlsl::ShaderModel::Kind::Callable: + return true; + default: + return false; + } +} + // Returns true if the given decl has the given semantic. bool hasSemantic(const DeclaratorDecl *decl, hlsl::DXIL::SemanticKind semanticKind) { @@ -803,6 +820,57 @@ void SpirvEmitter::HandleTranslationUnit(ASTContext &context) { return; } + // Pre-detect whether the resource-heap array stride must include the + // acceleration structure descriptor size. + // + // getResourceHeapArrayStride() caches its result on the first call. All + // resource runtime arrays created during code-gen share that one cached + // stride instruction pointer. To make the value correct the decision must + // be made here, before any descriptor-heap subscript expression is evaluated. + // + // Specifically, needsAccelStruct is set true under any of three conditions: + // + // Condition 1, ray-tracing entry point: any workQueue entry is an RT stage. + // RT stages unconditionally emit OpCapability RayTracingKHR; that capability + // is what permits OpConstantSizeOfEXT on AccelerationStructureKHR, so the + // instruction is safe to emit. + // + // Condition 2, explicit KHR_ray_tracing / NV_ray_tracing extension: a + // compute or graphics shader declared one of these extensions explicitly, + // meaning it may use acceleration structures via descriptor heap. + // + // Condition 3, explicit KHR_ray_query extension: same rationale for + // RayQuery users. + // + // isExtensionEnabled() cannot be used without the guard: in default mode + // (no -fspv-extension flags), FeatureManager enables all by-default + // extensions (including KHR_ray_tracing and KHR_ray_query) causing false + // positives. The guard !spirvOptions.allowedExtensions.empty() reliably + // distinguishes "user-listed" from "allowed by default." + // + // Note: the global-decl pass above runs before this block. HLSL forbids + // descriptor-heap access in global initializers, so + // getResourceHeapArrayStride() cannot be called there. If that restriction is + // ever lifted, this block should be moved before that pass. + if (spirvOptions.useDescriptorHeap) { + bool needsAccelStruct = false; + + for (const FunctionInfo *fi : workQueue) + if (shaderModelKindIsRayTracing(fi->shaderModelKind)) { + needsAccelStruct = true; + break; + } + + if (!needsAccelStruct && !spirvOptions.allowedExtensions.empty()) + needsAccelStruct = + featureManager.isExtensionEnabled(Extension::KHR_ray_tracing) || + featureManager.isExtensionEnabled(Extension::NV_ray_tracing) || + featureManager.isExtensionEnabled(Extension::KHR_ray_query); + + if (needsAccelStruct) + spvBuilder.noteResourceHeapHasAccelStruct(); + } + // Translate all functions reachable from the entry function. // The queue can grow in the meanwhile; so need to keep evaluating // workQueue.size(). @@ -7231,6 +7299,21 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, return nullptr; } + // The resource-heap stride is decided before the code-gen loop and is + // frozen on its first use, so it cannot be widened here. Reject rather + // than emit a stride that may be too narrow for the acceleration + // structure descriptor. + if (isRaytracingAccelerationStructure(resourceType) && + !spvBuilder.resourceHeapStrideIncludesAccelStruct()) { + emitError("acceleration structure loaded from ResourceDescriptorHeap " + "requires the resource heap stride to account for " + "acceleration structure descriptors; compile with " + "-fspv-extension=SPV_KHR_ray_tracing or " + "-fspv-extension=SPV_KHR_ray_query", + expr->getExprLoc()); + return nullptr; + } + if (isAKindOfStructuredOrByteBuffer(resourceType) || isConstantTextureBuffer(resourceType)) return emitDescriptorHeapBufferAccess(resourceType, var, index, expr, @@ -9683,13 +9766,19 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { const SpirvType * SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType) { - // SPV_EXT_descriptor_heap: apply a client-API-defined byte stride via an - // ArrayStrideIdEXT decoration. The sampler heap holds a single descriptor - // type, so its stride is the sampler descriptor size. The resource heap is a - // shared flat array in which any resource descriptor may sit at any slot, so - // every resource runtime array must use one common stride: max(sizeof(image), - // sizeof(buffer)). Using the accessed element size would be wrong for the - // resource heap. + // Apply a client-API-defined byte stride via an ArrayStrideIdEXT decoration. + // The sampler heap holds a single descriptor type, so its stride is the + // sampler descriptor size. The resource heap is a shared flat array in which + // any resource descriptor may sit at any slot, so every resource runtime + // array must use one common stride. + // + // [non-RT shaders]: + // max(sizeof(image), sizeof(buffer)) + // [RT shaders]: + // max(sizeof(image), sizeof(buffer), sizeof(acceleration_structure)) + // + // The stride is determined once before the code-gen loop and cached in + // spvBuilder. SpirvInstruction *strideId = isa(elemType) ? spvBuilder.getSamplerHeapArrayStride() : spvBuilder.getResourceHeapArrayStride(); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl new file mode 100644 index 0000000000..5c94f614b9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl @@ -0,0 +1,46 @@ +// RUN: not %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 -spirv %s 2>&1 | FileCheck %s + +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_query \ +// RUN: -spirv %s | FileCheck %s --check-prefix=OK + +// Verifies: an acceleration structure loaded from ResourceDescriptorHeap is +// rejected when the resource-heap stride was not widened to include the +// acceleration structure descriptor size. +// +// The stride is decided before the code-gen loop (see HandleTranslationUnit) +// and frozen on its first use, so it cannot be widened once an acceleration +// structure heap access is reached. Without the widening this shader would +// silently get max(sizeof(image), sizeof(buffer)), which may be too narrow. +// +// A compute shader is not a ray-tracing stage, so the widening only happens +// when the user lists a ray-tracing/ray-query extension explicitly. The first +// run does not, and must fail; the second one does, and must compile. + +// CHECK: error: acceleration structure loaded from ResourceDescriptorHeap requires the resource heap stride to account for acceleration structure descriptors; compile with -fspv-extension=SPV_KHR_ray_tracing or -fspv-extension=SPV_KHR_ray_query + +// OK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR +// OK-DAG: %[[AccelSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] + +RWBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; + + RayDesc ray; + ray.Origin = float3(0.0, 0.0, 0.0); + ray.Direction = float3(0.0, 0.0, 1.0); + ray.TMin = 0.0; + ray.TMax = 1000.0; + + RayQuery q; + q.TraceRayInline(scene, RAY_FLAG_NONE, 0xff, ray); + bool hit = q.Proceed(); + + output[tid.x] = float4(hit ? 1.0 : 0.0, 0.0, 0.0, 0.0); +} diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl new file mode 100644 index 0000000000..aabb9aa5b9 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -0,0 +1,154 @@ +// Two test paths share this file; select via -D RT_STAGE: +// +// Path A; Condition 1 (RT stage): +// -T lib_6_6 -D RT_STAGE -fspv-extension=SPV_KHR_ray_tracing +// A closesthit entry point is in the workQueue; shaderModelKindIsRayTracing() +// returns true and noteResourceHeapHasAccelStruct() is called unconditionally. +// +// Path B; Condition 3 (explicit KHR_ray_query extension): +// -T cs_6_6 -E main -fspv-extension=SPV_KHR_ray_query +// No RT stage in workQueue, so Condition 1 is skipped. The +// !spirvOptions.allowedExtensions.empty() guard passes (user listed an +// explicit extension), isExtensionEnabled(KHR_ray_query) is true, and +// noteResourceHeapHasAccelStruct() is called. +// +// Both paths verify that the shared resource-heap array stride expands to +// max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) +// and that ALL resource runtime arrays share that three-way max stride. +// +// Ordering stress test: Texture2D is accessed BEFORE the AS in source order. +// The stride cache must already hold sizeof(accel_struct) when the first +// runtime array type is created so the texture array uses the correct stride. +// +// The regression test for the "default-extension-mode" false positive (no +// -fspv-extension flags -> allowedExtensions is empty -> Condition 3 is skipped) +// is sm6_6.descriptorheap.ext.array-stride.hlsl, which must emit exactly 3 +// OpConstantSizeOfEXT (img, buf, sampler - no accel_struct). + +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_tracing \ +// RUN: -spirv %s | FileCheck %s --check-prefixes=CHECK,RT + +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_query \ +// RUN: -spirv %s | FileCheck %s + +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_tracing \ +// RUN: -spirv %s | FileCheck %s --check-prefix=SZRT + +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: -fspv-target-env=vulkan1.3 \ +// RUN: -fspv-extension=SPV_EXT_descriptor_heap \ +// RUN: -fspv-extension=SPV_KHR_untyped_pointers \ +// RUN: -fspv-extension=SPV_KHR_ray_query \ +// RUN: -spirv %s | FileCheck %s --check-prefix=SZRQ + +// --- Types (user-declared + stride-computation placeholders) --------------- +// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR +// CHECK-DAG: %[[Img:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown + +// UBuf is not user-declared; it is the canonical placeholder injected by +// getResourceHeapArrayStride() for the buffer descriptor size. +// CHECK-DAG: %[[UBuf:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform + +// Sampler only present in path A (closesthit). +// RT-DAG: %[[Samp:[a-zA-Z0-9_]+]] = OpTypeSampler + +// --- Runtime array types --------------------------------------------------- +// CHECK-DAG: %[[AccelArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] +// CHECK-DAG: %[[ImgArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Img]] + +// RT-DAG: %[[SampArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Samp]] + +// --- Size operands --------------------------------------------------------- +// CHECK-DAG: %[[ImgSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Img]] +// CHECK-DAG: %[[BufSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBuf]] +// CHECK-DAG: %[[ASSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] +// RT-DAG: %[[SampSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Samp]] + +// --- Three-way max: max(max(img, buf), accel) identical in both paths ---- +// CHECK-DAG: %[[IB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[MaxIB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IB]] %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[IBA:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[MaxIB]] %[[ASSz]] +// CHECK-DAG: %[[Stride:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IBA]] %[[MaxIB]] %[[ASSz]] + +// --- All resource arrays share the three-way-max stride ------------------- +// %[[ImgArr]] is created FIRST (texture before AS in source); most important. +// If the stride cache were populated before noteResourceHeapHasAccelStruct() +// was called, ImgArr would be decorated with a two-way max stride, which +// would not match %[[Stride]] (bound to the three-way max above), failing here. +// CHECK-DAG: OpDecorateId %[[ImgArr]] ArrayStrideIdEXT %[[Stride]] +// CHECK-DAG: OpDecorateId %[[AccelArr]] ArrayStrideIdEXT %[[Stride]] + +// --- Sampler stride is independent (path A only) -------------------------- +// RT-DAG: OpDecorateId %[[SampArr]] ArrayStrideIdEXT %[[SampSz]] + +// --- Exact OpConstantSizeOfEXT counts ------------------------------------- +// Path A: img, buf, accel, sampler = 4 +// SZRT-COUNT-4: OpConstantSizeOfEXT %uint +// SZRT-NOT: OpConstantSizeOfEXT %uint + +// Path B: img, buf, accel = 3 (no sampler) +// SZRQ-COUNT-3: OpConstantSizeOfEXT %uint +// SZRQ-NOT: OpConstantSizeOfEXT %uint + +#ifdef RT_STAGE + +struct Payload { float4 color; }; +struct Attribute { float2 bary; }; + +[shader("closesthit")] +void main(inout Payload payload, in Attribute attr) { + // Ordering stress test: access texture first + Texture2D tex = ResourceDescriptorHeap[0]; + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; + SamplerState samp = SamplerDescriptorHeap[0]; + + float4 color = tex.SampleLevel(samp, float2(0.0, 0.0), 0.0); + payload.color = color; + + RayDesc ray; + ray.Origin = float3(0.0, 0.0, 0.0); + ray.Direction = float3(0.0, 0.0, -1.0); + ray.TMin = 0.0; + ray.TMax = 1000.0; + + Payload child = { color }; + TraceRay(scene, 0x0, 0xff, 0, 1, 0, ray, child); +} + +#else // !RT_STAGE, compute shader with RayQuery (path B) + +RWBuffer output : register(u0); + +[numthreads(1, 1, 1)] +void main(uint3 tid : SV_DispatchThreadID) { + // Ordering stress test: access texture first + Texture2D tex = ResourceDescriptorHeap[0]; + RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; + + RayDesc ray; + ray.Origin = float3(0.0, 0.0, 0.0); + ray.Direction = float3(0.0, 0.0, 1.0); + ray.TMin = 0.0; + ray.TMax = 1000.0; + + RayQuery q; + q.TraceRayInline(scene, RAY_FLAG_NONE, 0xff, ray); + bool hit = q.Proceed(); + + int3 coord = int3(tid.x, 0, 0); + output[tid.x] = tex.Load(coord) + float4(hit ? 1.0 : 0.0, 0.0, 0.0, 0.0); +} + +#endif // RT_STAGE \ No newline at end of file From dac1c6a7f2c8f7870200df38772a3abb8041f36f Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Mon, 3 Aug 2026 10:03:44 -0700 Subject: [PATCH 43/45] Added AS reassignment diagnostic, fixed tests and docs --- docs/SPIR-V.rst | 61 ++++++-- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 14 ++ tools/clang/lib/SPIRV/DeclResultIdMapper.h | 7 + tools/clang/lib/SPIRV/SpirvEmitter.cpp | 43 ++++-- tools/clang/lib/SPIRV/SpirvEmitter.h | 5 +- ...t.acceleration-structure.stride.error.hlsl | 2 +- ...ptorheap.ext.array-stride.accelstruct.hlsl | 131 ++++++++++-------- 7 files changed, 178 insertions(+), 85 deletions(-) diff --git a/docs/SPIR-V.rst b/docs/SPIR-V.rst index ef0dd3a3a2..84e0f5eee4 100644 --- a/docs/SPIR-V.rst +++ b/docs/SPIR-V.rst @@ -2143,13 +2143,27 @@ handle into a function-scope image variable: %texel_ptr = OpUntypedImageTexelPointerEXT %uptr_image %image_type %descriptor %coord %sample %old = OpAtomicIAdd %uint %texel_ptr %scope %semantics %value +``RaytracingAccelerationStructure`` resources loaded from +``ResourceDescriptorHeap`` follow the same access-chain-then-load shape over a +runtime array of ``OpTypeAccelerationStructureKHR``: + +.. code:: spirv + + %accel_type = OpTypeAccelerationStructureKHR + %accel_array = OpTypeRuntimeArray %accel_type + OpDecorateId %accel_array ArrayStrideIdEXT %resource_stride + %descriptor = OpUntypedAccessChainKHR %uptr_uc %accel_array %resource_heap %index + %accel = OpLoad %accel_type %descriptor + This path supports texture, RWTexture, sampler, Buffer/RWBuffer, StructuredBuffer/RWStructuredBuffer without associated counter operations, -ByteAddressBuffer/RWByteAddressBuffer, ConstantBuffer, and TextureBuffer heap -loads, including direct field and array-element accesses for -``ConstantBuffer`` and ``TextureBuffer``. ``NonUniformResourceIndex`` is -accepted, but no ``NonUniform`` decoration is emitted on the -``OpUntypedAccessChainKHR`` result or on the loaded descriptor; +ByteAddressBuffer/RWByteAddressBuffer, ConstantBuffer, TextureBuffer, and +``RaytracingAccelerationStructure`` heap loads, including direct field and +array-element accesses for ``ConstantBuffer`` and ``TextureBuffer``. +Acceleration structure loads are additionally subject to the stride requirement +described in `Descriptor heap array stride`_ below. +``NonUniformResourceIndex`` is accepted, but no ``NonUniform`` decoration is +emitted on the ``OpUntypedAccessChainKHR`` result or on the loaded descriptor; ``SPV_EXT_descriptor_heap`` deprecates the decoration for heap accesses and drivers handle divergent heap indices natively. The index operand itself may still carry ``NonUniform`` from the surrounding expression. @@ -2185,11 +2199,13 @@ directly at each call or return site. Two further restrictions on the heap access expression itself produce diagnostics. The object being subscripted must be a direct reference to the -builtin ``ResourceDescriptorHeap`` or ``SamplerDescriptorHeap`` variable, and -the subscript result must be immediately converted to a concrete resource type -so that DXC can select a descriptor type for the access. A subscript whose -result is discarded, or used in a context that supplies no target resource -type, is rejected. +builtin ``ResourceDescriptorHeap`` or ``SamplerDescriptorHeap`` variable; the +subscript result must be immediately converted to a concrete resource type so +that DXC can select a descriptor type for the access, so a subscript whose +result is discarded or used in a context that supplies no target resource type +is rejected; and a local ``RaytracingAccelerationStructure`` must be initialized +from a loadable heap access, since the alias has no backing descriptor +otherwise. Descriptor heap array stride ++++++++++++++++++++++++++++ @@ -2199,7 +2215,8 @@ rather than a literal ``ArrayStride``, because descriptor sizes are not known until pipeline creation. The shared value is built from ``OpConstantSizeOfEXT`` and ``OpSpecConstantOp`` and evaluates to ``max(sizeof(image_descriptor), sizeof(buffer_descriptor))``. The sampler heap -carries its own ``ArrayStrideIdEXT`` equal to ``sizeof(sampler_descriptor)``. +carries its own ``ArrayStrideIdEXT`` equal to ``sizeof(sampler_descriptor)``, +regardless of resource heap contents. The ``OpConstantSizeOfEXT`` operands are placeholder types chosen only for their descriptor class. All image types report the same descriptor size, so the @@ -2208,6 +2225,28 @@ types the shader actually uses; a module will normally contain both the placeholder type and the distinct image types its heap accesses lower to. The stride value is built once and cached on first use. +When acceleration structure descriptors may appear on the resource heap, the +formula expands to a three-way max +``max(max(sizeof(image_descriptor), sizeof(buffer_descriptor)), sizeof(acceleration_structure))``. + +Because the stride is cached on first use, this decision is committed before +code generation and is **not** based on whether the shader actually performs an +acceleration structure heap load: a ray-tracing shader that only heap-loads a +texture still gets the three-way max. The widening happens when either + +- any entry point is a ray-tracing stage, or +- the user explicitly passed ``-fspv-extension=SPV_KHR_ray_tracing``, + ``-fspv-extension=SPV_NV_ray_tracing``, or + ``-fspv-extension=SPV_KHR_ray_query``. + +The second condition requires an explicit ``-fspv-extension`` flag. In the +default extension mode DXC allows the ray-tracing and ray-query extensions +implicitly, but that does not widen the stride. Because the stride cannot be +widened once it has been built, a shader that is not a ray-tracing stage and +loads a ``RaytracingAccelerationStructure`` from ``ResourceDescriptorHeap`` +without an explicit ray extension flag is rejected rather than given a stride +that may be too narrow. + HLSL Expressions ================ diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 3a88334ffc..4660e8a9e0 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -1136,6 +1136,20 @@ DeclResultIdMapper::createFnVar(const VarDecl *var, return varInstr; } +void DeclResultIdMapper::registerFnVarAlias(const VarDecl *var, + SpirvInstruction *varInstr) { + if (varInstr) + registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); +} + +bool DeclResultIdMapper::hasFnVarAlias(const VarDecl *var) const { + const DeclSpirvInfo *info = getDeclSpirvInfo(var); + if (!info || !info->instr) + return false; + return !isa(info->instr); +} + + SpirvDebugGlobalVariable *DeclResultIdMapper::createDebugGlobalVariable( SpirvVariable *var, const QualType &type, const SourceLocation &loc, const StringRef &name) { diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.h b/tools/clang/lib/SPIRV/DeclResultIdMapper.h index 6a96ae8f45..9fa590efbf 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.h +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.h @@ -285,6 +285,13 @@ class DeclResultIdMapper { SpirvVariable *createFnVar(const VarDecl *var, llvm::Optional init); + /// \brief Registers a function-scope alias to an existing instruction. + void registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr); + + /// \brief Returns true if the decl was registered via registerFnVarAlias, + bool hasFnVarAlias(const VarDecl *var) const; + + /// \brief Creates a file-scope variable and returns its instruction. SpirvVariable *createFileVar(const VarDecl *var, llvm::Optional init); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 816a32abb1..28e1679d3b 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -2103,12 +2103,17 @@ bool SpirvEmitter::tryToCreateDescriptorHeapAlias(const VarDecl *decl, } if (isRaytracingAccelerationStructure(decl->getType())) { - if (auto *initVal = loadIfGLValue(init)) + if (SpirvInstruction *initVal = loadIfGLValue(init)) { declIdMapper.registerFnVarAlias(decl, initVal); - else + // Track the AS as heap-initialized so diagnoseDescriptorHeapAliasMixing + // can detect any later reassignment (even heap-to-heap, since + // registerFnVarAlias cannot be updated after the fact). + descriptorHeapVarState[decl] = DescriptorHeapVarState::Heap; + } else { emitError("cannot create descriptor heap acceleration structure alias " "from initializer", init->getExprLoc()); + } return true; } @@ -3301,11 +3306,14 @@ SpirvEmitter::tryToAssignToDescriptorHeapAlias( assignExpr->getExprLoc())) return tryToAssignToDescriptorHeapBuffer(assignExpr); - // The assignment was rejected. A heap buffer alias is represented only by its - // index variable, so the normal path has no destination to store into and - // would null-deref; consume the assignment instead. Image aliases do have a - // function variable, so they can fall through to the plain handle store. - if (descriptorHeapBufferAliasVars.count(dstVar)) + // The assignment was rejected. Buffer aliases (index-only, no backing + // function variable) and AS aliases (value registered via registerFnVarAlias, + // not a SpirvVariable) have no valid destination for processAssignment; + // consume the assignment to prevent a null-deref. Image aliases do have a + // backing function variable, so they can fall through to the plain handle + // store. + if (descriptorHeapBufferAliasVars.count(dstVar) || + declIdMapper.hasFnVarAlias(dstVar)) return static_cast(nullptr); return llvm::None; } @@ -5372,19 +5380,29 @@ bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, // Only the resource kinds that use the compile-time alias mechanism can be // miscompiled by a mixed assignment. Other resources are stored into a real // function variable, which merges correctly across control flow. + // RaytracingAccelerationStructure uses registerFnVarAlias and is covered + // here too; its Heap state is set in tryToCreateDescriptorHeapAlias. const QualType dstType = dstVar->getType(); - if (!isRWTexture(dstType) && !isRWBuffer(dstType) && + const bool isASType = isRaytracingAccelerationStructure(dstType); + if (!isASType && !isRWTexture(dstType) && !isRWBuffer(dstType) && !isConstantTextureBuffer(dstType) && !isAKindOfStructuredOrByteBuffer(dstType)) return false; const bool srcIsHeap = isHeapSourcedValue(srcExpr->IgnoreParenCasts()); const bool wasHeap = descriptorHeapImageAliasVars.count(dstVar) || - descriptorHeapBufferAliasVars.count(dstVar); + descriptorHeapBufferAliasVars.count(dstVar) || + (stateIt != descriptorHeapVarState.end() && + stateIt->second == DescriptorHeapVarState::Heap); const bool wasBound = stateIt != descriptorHeapVarState.end() && stateIt->second == DescriptorHeapVarState::Bound; - const bool mixingDetected = - (srcIsHeap && wasBound) || (!srcIsHeap && wasHeap); + // AS aliases cannot be updated after initialization (registerFnVarAlias is + // frozen), so any reassignment (including heap-to-heap) must be rejected. + // Image and buffer aliases support heap-to-heap reassignment via their + // respective alias-update paths, so only the cross-kind cases are errors. + const bool mixingDetected = (isASType && wasHeap) || + (srcIsHeap && wasBound) || + (!srcIsHeap && wasHeap); if (mixingDetected) { emitError("mixing bound and descriptor heap resources in the same variable " @@ -7308,7 +7326,8 @@ SpirvEmitter::doCXXOperatorCallExpr(const CXXOperatorCallExpr *expr, emitError("acceleration structure loaded from ResourceDescriptorHeap " "requires the resource heap stride to account for " "acceleration structure descriptors; compile with " - "-fspv-extension=SPV_KHR_ray_tracing or " + "-fspv-extension=SPV_KHR_ray_tracing, " + "-fspv-extension=SPV_NV_ray_tracing, or " "-fspv-extension=SPV_KHR_ray_query", expr->getExprLoc()); return nullptr; diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 3c83ef7760..13e833ea78 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -1744,11 +1744,14 @@ class SpirvEmitter : public ASTConsumer { bool counterUnsupported = false; }; /// Tracks per-variable assignment history for the mixed-alias diagnostic. + /// Heap: the variable was initialized from ResourceDescriptorHeap. For AS + /// variables this is the only state that can follow initialization, + /// since registerFnVarAlias cannot be updated after the fact. /// Bound: the variable has been assigned from a bound resource; a later heap /// assignment to it would be a diagnosable mix. /// Mixed: mixing was already diagnosed; alias recording stays suppressed for /// the remainder of the function so the error fires only once. - enum class DescriptorHeapVarState : uint8_t { Bound, Mixed }; + enum class DescriptorHeapVarState : uint8_t { Heap, Bound, Mixed }; llvm::DenseMap descriptorHeapVarState; diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl index 5c94f614b9..26519a5cb8 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.acceleration-structure.stride.error.hlsl @@ -21,7 +21,7 @@ // when the user lists a ray-tracing/ray-query extension explicitly. The first // run does not, and must fail; the second one does, and must compile. -// CHECK: error: acceleration structure loaded from ResourceDescriptorHeap requires the resource heap stride to account for acceleration structure descriptors; compile with -fspv-extension=SPV_KHR_ray_tracing or -fspv-extension=SPV_KHR_ray_query +// CHECK: error: acceleration structure loaded from ResourceDescriptorHeap requires the resource heap stride to account for acceleration structure descriptors; compile with -fspv-extension=SPV_KHR_ray_tracing, -fspv-extension=SPV_NV_ray_tracing, or -fspv-extension=SPV_KHR_ray_query // OK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR // OK-DAG: %[[AccelSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl index aabb9aa5b9..5d39a64730 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -1,104 +1,115 @@ -// Two test paths share this file; select via -D RT_STAGE: +// Verifies the descriptor-heap array stride when a RaytracingAccelerationStructure +// is present in the resource heap. // -// Path A; Condition 1 (RT stage): -// -T lib_6_6 -D RT_STAGE -fspv-extension=SPV_KHR_ray_tracing -// A closesthit entry point is in the workQueue; shaderModelKindIsRayTracing() -// returns true and noteResourceHeapHasAccelStruct() is called unconditionally. +// When acceleration structures are accessed from ResourceDescriptorHeap, the +// stride formula expands from max(sizeof(image), sizeof(buffer)) to the +// three-way max: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)). +// All resource runtime arrays must share this wider stride so that any slot +// can hold any descriptor type. // -// Path B; Condition 3 (explicit KHR_ray_query extension): -// -T cs_6_6 -E main -fspv-extension=SPV_KHR_ray_query -// No RT stage in workQueue, so Condition 1 is skipped. The -// !spirvOptions.allowedExtensions.empty() guard passes (user listed an -// explicit extension), isExtensionEnabled(KHR_ray_query) is true, and -// noteResourceHeapHasAccelStruct() is called. +// The compiler widens the stride under two conditions: // -// Both paths verify that the shared resource-heap array stride expands to -// max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) -// and that ALL resource runtime arrays share that three-way max stride. +// Path A (RT stage): the shader model is a ray-tracing stage, so any heap +// access may load an acceleration structure. Ray-tracing extensions are +// requested explicitly via -fspv-extension=SPV_KHR_ray_tracing. +// +// Path B (ray query): the SPV_KHR_ray_query extension is explicitly requested +// by the user, signalling that AS descriptors may appear in the heap even +// without a ray-tracing stage. // // Ordering stress test: Texture2D is accessed BEFORE the AS in source order. -// The stride cache must already hold sizeof(accel_struct) when the first -// runtime array type is created so the texture array uses the correct stride. +// If the stride cache were populated before the AS widens it, the texture +// runtime array would be decorated with the narrower two-way max stride. // -// The regression test for the "default-extension-mode" false positive (no -// -fspv-extension flags -> allowedExtensions is empty -> Condition 3 is skipped) -// is sm6_6.descriptorheap.ext.array-stride.hlsl, which must emit exactly 3 -// OpConstantSizeOfEXT (img, buf, sampler - no accel_struct). +// For the no-AS baseline (exactly 3 OpConstantSizeOfEXT: img, buf, sampler) +// see sm6_6.descriptorheap.ext.array-stride.hlsl. -// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefixes=CHECK,RT -// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ // RUN: -spirv %s | FileCheck %s -// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefix=SZRT -// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ // RUN: -spirv %s | FileCheck %s --check-prefix=SZRQ -// --- Types (user-declared + stride-computation placeholders) --------------- -// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR -// CHECK-DAG: %[[Img:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown +// Element (descriptor) types. +// CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR + +// Two distinct OpTypeImage types appear in the output: +// +// 1) ImgPlaceholder: a canonical sampled 2D float image (depth=0) used only as +// the operand to OpConstantSizeOfEXT. All image subtypes report the same +// imageDescriptorSize, so the specific depth field is irrelevant to the query. +// +// 2) TexDesc: the actual lowered type for Texture2D (depth=2, +// WithDepth::Unknown), produced by LowerTypeVisitor for sampled textures. +// This is the element type of the texture heap runtime array. +// +// CHECK-DAG: %[[ImgPlaceholder:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 0 0 0 1 Unknown +// CHECK-DAG: %[[TexDesc:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown -// UBuf is not user-declared; it is the canonical placeholder injected by -// getResourceHeapArrayStride() for the buffer descriptor size. -// CHECK-DAG: %[[UBuf:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform +// UBuf is the canonical buffer placeholder injected for the buffer descriptor +// size query; it is not user-declared. +// CHECK-DAG: %[[UBuf:[a-zA-Z0-9_]+]] = OpTypeBufferEXT Uniform -// Sampler only present in path A (closesthit). +// Sampler only present in path A (RT stage). // RT-DAG: %[[Samp:[a-zA-Z0-9_]+]] = OpTypeSampler -// --- Runtime array types --------------------------------------------------- -// CHECK-DAG: %[[AccelArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] -// CHECK-DAG: %[[ImgArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Img]] - +// Heap runtime arrays of the accessed element types. +// CHECK-DAG: %[[AccelArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Accel]] +// CHECK-DAG: %[[ImgArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[TexDesc]] // RT-DAG: %[[SampArr:[a-zA-Z0-9_]+]] = OpTypeRuntimeArray %[[Samp]] -// --- Size operands --------------------------------------------------------- -// CHECK-DAG: %[[ImgSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Img]] -// CHECK-DAG: %[[BufSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBuf]] -// CHECK-DAG: %[[ASSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] +// Resource stride = max(max(image_size, buffer_size), accel_size). +// The size query uses ImgPlaceholder (depth=0); the driver returns the same +// imageDescriptorSize regardless of which image subtype is used as the operand. +// CHECK-DAG: %[[ImgSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[ImgPlaceholder]] +// CHECK-DAG: %[[BufSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[UBuf]] +// CHECK-DAG: %[[ASSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Accel]] // RT-DAG: %[[SampSz:[a-zA-Z0-9_]+]] = OpConstantSizeOfEXT %uint %[[Samp]] -// --- Three-way max: max(max(img, buf), accel) identical in both paths ---- -// CHECK-DAG: %[[IB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSz]] %[[BufSz]] -// CHECK-DAG: %[[MaxIB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IB]] %[[ImgSz]] %[[BufSz]] -// CHECK-DAG: %[[IBA:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[MaxIB]] %[[ASSz]] -// CHECK-DAG: %[[Stride:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IBA]] %[[MaxIB]] %[[ASSz]] - -// --- All resource arrays share the three-way-max stride ------------------- -// %[[ImgArr]] is created FIRST (texture before AS in source); most important. -// If the stride cache were populated before noteResourceHeapHasAccelStruct() -// was called, ImgArr would be decorated with a two-way max stride, which -// would not match %[[Stride]] (bound to the three-way max above), failing here. -// CHECK-DAG: OpDecorateId %[[ImgArr]] ArrayStrideIdEXT %[[Stride]] -// CHECK-DAG: OpDecorateId %[[AccelArr]] ArrayStrideIdEXT %[[Stride]] - -// --- Sampler stride is independent (path A only) -------------------------- +// Three-way max: max(max(img, buf), accel), identical in both paths. +// CHECK-DAG: %[[IB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[MaxIB:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IB]] %[[ImgSz]] %[[BufSz]] +// CHECK-DAG: %[[IBA:[a-zA-Z0-9_]+]] = OpSpecConstantOp %bool UGreaterThan %[[MaxIB]] %[[ASSz]] +// CHECK-DAG: %[[Stride:[a-zA-Z0-9_]+]] = OpSpecConstantOp %uint Select %[[IBA]] %[[MaxIB]] %[[ASSz]] + +// All resource arrays share the three-way-max stride. +// %[[ImgArr]] is created FIRST (texture before AS in source order); if the +// stride were not yet widened at array-type creation time, this decoration +// would reference the narrower two-way max and the check below would fail. +// CHECK-DAG: OpDecorateId %[[ImgArr]] ArrayStrideIdEXT %[[Stride]] +// CHECK-DAG: OpDecorateId %[[AccelArr]] ArrayStrideIdEXT %[[Stride]] + +// Sampler stride is independent (path A only). // RT-DAG: OpDecorateId %[[SampArr]] ArrayStrideIdEXT %[[SampSz]] -// --- Exact OpConstantSizeOfEXT counts ------------------------------------- -// Path A: img, buf, accel, sampler = 4 +// Exact OpConstantSizeOfEXT counts. +// Path A (RT stage): img, buf, accel, sampler = 4. // SZRT-COUNT-4: OpConstantSizeOfEXT %uint // SZRT-NOT: OpConstantSizeOfEXT %uint -// Path B: img, buf, accel = 3 (no sampler) +// Path B (ray query): img, buf, accel = 3 (no sampler). // SZRQ-COUNT-3: OpConstantSizeOfEXT %uint // SZRQ-NOT: OpConstantSizeOfEXT %uint @@ -109,7 +120,7 @@ struct Attribute { float2 bary; }; [shader("closesthit")] void main(inout Payload payload, in Attribute attr) { - // Ordering stress test: access texture first + // Ordering stress test: access texture first. Texture2D tex = ResourceDescriptorHeap[0]; RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; SamplerState samp = SamplerDescriptorHeap[0]; @@ -133,7 +144,7 @@ RWBuffer output : register(u0); [numthreads(1, 1, 1)] void main(uint3 tid : SV_DispatchThreadID) { - // Ordering stress test: access texture first + // Ordering stress test: access texture first. Texture2D tex = ResourceDescriptorHeap[0]; RaytracingAccelerationStructure scene = ResourceDescriptorHeap[1]; @@ -151,4 +162,4 @@ void main(uint3 tid : SV_DispatchThreadID) { output[tid.x] = tex.Load(coord) + float4(hit ? 1.0 : 0.0, 0.0, 0.0, 0.0); } -#endif // RT_STAGE \ No newline at end of file +#endif // RT_STAGE From 7bb90dffdbdfeb41c57f1f860271d111d3a449c7 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 21 Aug 2026 10:40:30 -0700 Subject: [PATCH 44/45] fix test --- ...ptorheap.ext.array-stride.accelstruct.hlsl | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl index 5d39a64730..2189a0f618 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -50,7 +50,7 @@ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ -// RUN: -spirv %s | FileCheck %s --check-prefix=SZRQ +// RUN: -spirv %s | FileCheck %s --check-prefixes=SZRQ,NOSAMP // Element (descriptor) types. // CHECK-DAG: %[[Accel:[a-zA-Z0-9_]+]] = OpTypeAccelerationStructureKHR @@ -104,14 +104,21 @@ // Sampler stride is independent (path A only). // RT-DAG: OpDecorateId %[[SampArr]] ArrayStrideIdEXT %[[SampSz]] -// Exact OpConstantSizeOfEXT counts. -// Path A (RT stage): img, buf, accel, sampler = 4. -// SZRT-COUNT-4: OpConstantSizeOfEXT %uint -// SZRT-NOT: OpConstantSizeOfEXT %uint - -// Path B (ray query): img, buf, accel = 3 (no sampler). -// SZRQ-COUNT-3: OpConstantSizeOfEXT %uint -// SZRQ-NOT: OpConstantSizeOfEXT %uint +// Path A (RT stage) queries img, buf, accel and sampler; path B (ray query) +// queries img, buf and accel. Each expected query is asserted by a CHECK-DAG +// above, so what remains is to rule out the extra queries that a per-element or +// per-access implementation would emit. +// +// In both paths the lowered Texture2D type must never be measured, because one +// canonical image placeholder covers every image descriptor. +// SZRT-DAG: %[[SZRT_Tex:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// SZRT-NOT: OpConstantSizeOfEXT %uint %[[SZRT_Tex]] +// SZRQ-DAG: %[[SZRQ_Tex:[a-zA-Z0-9_]+]] = OpTypeImage %float 2D 2 0 0 1 Unknown +// SZRQ-NOT: OpConstantSizeOfEXT %uint %[[SZRQ_Tex]] + +// Path B declares no sampler, so no sampler descriptor type reaches the module +// and therefore no sampler size query can be emitted. +// NOSAMP-NOT: OpTypeSampler #ifdef RT_STAGE From ac88333219792192985f9a293a646fd9014b3a61 Mon Sep 17 00:00:00 2001 From: Jonathan Zakharov Date: Fri, 11 Sep 2026 10:02:55 -0700 Subject: [PATCH 45/45] improve comments and convention compliance --- docs/ReleaseNotes.md | 7 +- .../clang/include/clang/SPIRV/SpirvBuilder.h | 32 +++---- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 5 +- tools/clang/lib/SPIRV/DeclResultIdMapper.h | 3 +- tools/clang/lib/SPIRV/SpirvBuilder.cpp | 3 +- tools/clang/lib/SPIRV/SpirvEmitter.cpp | 87 +++++++------------ tools/clang/lib/SPIRV/SpirvEmitter.h | 11 ++- ...ptorheap.ext.array-stride.accelstruct.hlsl | 47 +++++----- 8 files changed, 90 insertions(+), 105 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index cc65e3752a..11d13c394f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -24,9 +24,10 @@ The included licenses apply to the following files: - Extended `SPV_EXT_descriptor_heap` lowering (`-fspv-use-descriptor-heap`) to buffer-like resources (`StructuredBuffer`, `ByteAddressBuffer`, `ConstantBuffer`, `TextureBuffer`, and RW variants) via - `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3` - [#8517](https://github.com/microsoft/DirectXShaderCompiler/pull/8517). - + `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3`. +- Extended `-fspv-use-descriptor-heap` to support `RaytracingAccelerationStructure` + loaded from `ResourceDescriptorHeap`. + Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. diff --git a/tools/clang/include/clang/SPIRV/SpirvBuilder.h b/tools/clang/include/clang/SPIRV/SpirvBuilder.h index 2e6b49bd17..ad96512e5f 100644 --- a/tools/clang/include/clang/SPIRV/SpirvBuilder.h +++ b/tools/clang/include/clang/SPIRV/SpirvBuilder.h @@ -843,26 +843,26 @@ class SpirvBuilder { SpirvInstruction *op3, SourceLocation loc); /// \brief Record that acceleration structures may occupy the resource heap. - /// Note: Must be called before getResourceHeapArrayStride() (before the - /// code-gen loop in HandleTranslationUnit) so the cached stride is correct - /// on the first call. Calling it later has no effect because the result is - /// frozen after the first getResourceHeapArrayStride() invocation. + /// Must be called before getResourceHeapArrayStride() (before the code-gen + /// loop in HandleTranslationUnit); the result is cached on the first call. + /// Calling it after the first getResourceHeapArrayStride() invocation has + /// no effect. void noteResourceHeapHasAccelStruct() { resourceHeapHasAccelStruct = true; } - /// \brief Returns whether the resource-heap stride accounts for acceleration - /// structure descriptors. Code-gen must reject an acceleration structure heap - /// access when this is false: the decision is made before the code-gen loop - /// and the stride cannot be widened afterwards. + /// \brief Returns whether the resource-heap stride includes the acceleration + /// structure descriptor size. When false, code-gen must reject AS heap + /// access; the stride is frozen after getResourceHeapArrayStride()'s first + /// call. bool resourceHeapStrideIncludesAccelStruct() const { return resourceHeapHasAccelStruct; } - /// \brief Shared ArrayStrideIdEXT operand for resource-heap runtime arrays. + /// \brief Shared ArrayStrideIdEXT for resource-heap runtime arrays. /// Default: max(sizeof(image), sizeof(buffer)) - /// With RT: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) - /// Computed via OpSpecConstantOp and cached per module. - /// Note: noteResourceHeapHasAccelStruct() must be called before this if AS - /// may be present (result is frozen on the first call). + /// With RT: max(sizeof(image), sizeof(buffer), sizeof(accel_struct)) + /// Via OpSpecConstantOp; cached per module. + /// Precondition: call noteResourceHeapHasAccelStruct() before first call if + /// AS present. SpirvInstruction *getResourceHeapArrayStride(); /// \brief Shared ArrayStrideIdEXT operand for sampler-heap runtime arrays: @@ -994,9 +994,9 @@ class SpirvBuilder { SpirvInstruction *samplerHeapArrayStride = nullptr; /// Set by noteResourceHeapHasAccelStruct() when HandleTranslationUnit - /// detects that the shader uses ray-tracing features. When true, - /// getResourceHeapArrayStride() extends the stride to include - /// sizeof(acceleration_structure). + /// detects shader using ray-tracing features. + /// When true, getResourceHeapArrayStride() includes + /// sizeof(acceleration_structure). Set by noteResourceHeapHasAccelStruct(). bool resourceHeapHasAccelStruct = false; SpirvDebugInfoNone *debugNone; diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 4660e8a9e0..62246ab0b7 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -1138,8 +1138,8 @@ DeclResultIdMapper::createFnVar(const VarDecl *var, void DeclResultIdMapper::registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr) { - if (varInstr) - registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); + assert(varInstr); + registerVariableForDecl(var, createDeclSpirvInfo(varInstr)); } bool DeclResultIdMapper::hasFnVarAlias(const VarDecl *var) const { @@ -1149,7 +1149,6 @@ bool DeclResultIdMapper::hasFnVarAlias(const VarDecl *var) const { return !isa(info->instr); } - SpirvDebugGlobalVariable *DeclResultIdMapper::createDebugGlobalVariable( SpirvVariable *var, const QualType &type, const SourceLocation &loc, const StringRef &name) { diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.h b/tools/clang/lib/SPIRV/DeclResultIdMapper.h index 9fa590efbf..df92ea96a7 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.h +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.h @@ -288,10 +288,9 @@ class DeclResultIdMapper { /// \brief Registers a function-scope alias to an existing instruction. void registerFnVarAlias(const VarDecl *var, SpirvInstruction *varInstr); - /// \brief Returns true if the decl was registered via registerFnVarAlias, + /// \brief Returns true if the decl was registered via registerFnVarAlias. bool hasFnVarAlias(const VarDecl *var) const; - /// \brief Creates a file-scope variable and returns its instruction. SpirvVariable *createFileVar(const VarDecl *var, llvm::Optional init); diff --git a/tools/clang/lib/SPIRV/SpirvBuilder.cpp b/tools/clang/lib/SPIRV/SpirvBuilder.cpp index 3e16ee46de..b98bb49b70 100644 --- a/tools/clang/lib/SPIRV/SpirvBuilder.cpp +++ b/tools/clang/lib/SPIRV/SpirvBuilder.cpp @@ -2094,9 +2094,8 @@ SpirvInstruction *SpirvBuilder::getResourceHeapArrayStride() { resourceHeapArrayStride = createSpecConstantTernaryOp(spv::Op::OpSelect, astContext.UnsignedIntTy, maxImgBufIsBigger, maxImgBuf, asSize, {}); - } else { + } else resourceHeapArrayStride = maxImgBuf; - } return resourceHeapArrayStride; } diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.cpp b/tools/clang/lib/SPIRV/SpirvEmitter.cpp index 28e1679d3b..bb548c3432 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.cpp +++ b/tools/clang/lib/SPIRV/SpirvEmitter.cpp @@ -67,9 +67,8 @@ bool isImplicitVarDeclInVkNamespace(const Decl *decl) { return false; } -// Returns true if the given shader model kind is a ray-tracing stage. -// Used to detect whether the resource heap stride must include -// sizeof(acceleration_structure). +// Returns true if k is an RT stage; determines whether the resource-heap +// stride must include sizeof(acceleration_structure). bool shaderModelKindIsRayTracing(hlsl::ShaderModel::Kind k) { switch (k) { case hlsl::ShaderModel::Kind::RayGeneration: @@ -820,38 +819,25 @@ void SpirvEmitter::HandleTranslationUnit(ASTContext &context) { return; } - // Pre-detect whether the resource-heap array stride must include the - // acceleration structure descriptor size. + // Pre-detect whether the resource-heap stride must include the acceleration + // structure size. getResourceHeapArrayStride() caches on first call, so the + // decision must precede any descriptor-heap subscript evaluation. // - // getResourceHeapArrayStride() caches its result on the first call. All - // resource runtime arrays created during code-gen share that one cached - // stride instruction pointer. To make the value correct the decision must - // be made here, before any descriptor-heap subscript expression is evaluated. + // needsAccelStruct is set when any of three conditions holds: + // C1: workQueue has an RT stage. RT stages emit RayTracingKHR, which + // permits OpConstantSizeOfEXT on AccelerationStructureKHR. + // C2: shader explicitly listed KHR_ray_tracing or NV_ray_tracing — + // signals the shader may use acceleration structures via descriptor + // heap. + // C3: shader explicitly listed KHR_ray_query — same rationale. // - // Specifically, needsAccelStruct is set true under any of three conditions: + // isExtensionEnabled() requires the !allowedExtensions.empty() guard: in + // default mode FeatureManager enables KHR_ray_tracing and KHR_ray_query + // by default, causing false positives without it. // - // Condition 1, ray-tracing entry point: any workQueue entry is an RT stage. - // RT stages unconditionally emit OpCapability RayTracingKHR; that capability - // is what permits OpConstantSizeOfEXT on AccelerationStructureKHR, so the - // instruction is safe to emit. - // - // Condition 2, explicit KHR_ray_tracing / NV_ray_tracing extension: a - // compute or graphics shader declared one of these extensions explicitly, - // meaning it may use acceleration structures via descriptor heap. - // - // Condition 3, explicit KHR_ray_query extension: same rationale for - // RayQuery users. - // - // isExtensionEnabled() cannot be used without the guard: in default mode - // (no -fspv-extension flags), FeatureManager enables all by-default - // extensions (including KHR_ray_tracing and KHR_ray_query) causing false - // positives. The guard !spirvOptions.allowedExtensions.empty() reliably - // distinguishes "user-listed" from "allowed by default." - // - // Note: the global-decl pass above runs before this block. HLSL forbids - // descriptor-heap access in global initializers, so - // getResourceHeapArrayStride() cannot be called there. If that restriction is - // ever lifted, this block should be moved before that pass. + // Note: global-decl pass runs before this block. HLSL forbids heap access + // in global initializers (getResourceHeapArrayStride() cannot be called + // there). If that restriction lifts, move this block before that pass. if (spirvOptions.useDescriptorHeap) { bool needsAccelStruct = false; @@ -3306,12 +3292,10 @@ SpirvEmitter::tryToAssignToDescriptorHeapAlias( assignExpr->getExprLoc())) return tryToAssignToDescriptorHeapBuffer(assignExpr); - // The assignment was rejected. Buffer aliases (index-only, no backing - // function variable) and AS aliases (value registered via registerFnVarAlias, - // not a SpirvVariable) have no valid destination for processAssignment; - // consume the assignment to prevent a null-deref. Image aliases do have a - // backing function variable, so they can fall through to the plain handle - // store. + // Assignment rejected. Buffer aliases (index-only, no backing variable) and + // AS aliases (registerFnVarAlias, not SpirvVariable) have no + // processAssignment destination; consume to prevent null-deref. Image aliases + // have a backing variable; fall through to handle store. if (descriptorHeapBufferAliasVars.count(dstVar) || declIdMapper.hasFnVarAlias(dstVar)) return static_cast(nullptr); @@ -5396,10 +5380,9 @@ bool SpirvEmitter::diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, stateIt->second == DescriptorHeapVarState::Heap); const bool wasBound = stateIt != descriptorHeapVarState.end() && stateIt->second == DescriptorHeapVarState::Bound; - // AS aliases cannot be updated after initialization (registerFnVarAlias is - // frozen), so any reassignment (including heap-to-heap) must be rejected. - // Image and buffer aliases support heap-to-heap reassignment via their - // respective alias-update paths, so only the cross-kind cases are errors. + // AS aliases are frozen (registerFnVarAlias cannot be updated), so any + // reassignment — including heap-to-heap — is rejected. Image/buffer aliases + // support heap-to-heap reassignment; only cross-kind (bound↔heap) is flagged. const bool mixingDetected = (isASType && wasHeap) || (srcIsHeap && wasBound) || (!srcIsHeap && wasHeap); @@ -9785,19 +9768,13 @@ void SpirvEmitter::createSpecConstant(const VarDecl *varDecl) { const SpirvType * SpirvEmitter::getDescriptorHeapRuntimeArrayType(const SpirvType *elemType) { - // Apply a client-API-defined byte stride via an ArrayStrideIdEXT decoration. - // The sampler heap holds a single descriptor type, so its stride is the - // sampler descriptor size. The resource heap is a shared flat array in which - // any resource descriptor may sit at any slot, so every resource runtime - // array must use one common stride. - // - // [non-RT shaders]: - // max(sizeof(image), sizeof(buffer)) - // [RT shaders]: - // max(sizeof(image), sizeof(buffer), sizeof(acceleration_structure)) - // - // The stride is determined once before the code-gen loop and cached in - // spvBuilder. + // Apply a client-API-defined byte stride via ArrayStrideIdEXT. + // Sampler heap holds one descriptor type; stride = sampler descriptor size. + // Resource heap is a shared flat array; all runtime arrays share one stride: + // non-RT: max(sizeof(image), sizeof(buffer)) + // RT: max(sizeof(image), sizeof(buffer), + // sizeof(acceleration_structure)) + // Determined once before the code-gen loop; cached in spvBuilder. SpirvInstruction *strideId = isa(elemType) ? spvBuilder.getSamplerHeapArrayStride() : spvBuilder.getResourceHeapArrayStride(); diff --git a/tools/clang/lib/SPIRV/SpirvEmitter.h b/tools/clang/lib/SPIRV/SpirvEmitter.h index 13e833ea78..380f5b8ebb 100644 --- a/tools/clang/lib/SPIRV/SpirvEmitter.h +++ b/tools/clang/lib/SPIRV/SpirvEmitter.h @@ -1281,7 +1281,16 @@ class SpirvEmitter : public ASTConsumer { /// \brief Diagnoses a local resource variable assigned from both a bound /// resource and ResourceDescriptorHeap. /// - /// Issues a diagnostic and returns true if rejected. + /// The per-resource alias maps (descriptorHeapImageAliasVars, + /// descriptorHeapBufferAliasVars, registerFnVarAlias for AS) record each + /// aliased VarDecl at compile time; every later use is unconditionally + /// re-lowered as a heap access chain, ignoring control flow. Correct only + /// when the variable holds a heap descriptor on every reaching path; + /// diagnose mixed assignments rather than miscompile. + /// + /// Returns true if rejected (new diagnostic emitted or variable already + /// diagnosed); callers need not act, alias-recording helpers check + /// descriptorHeapVarState and skip rejected variables automatically. bool diagnoseDescriptorHeapAliasMixing(const VarDecl *dstVar, const Expr *srcExpr, SourceLocation loc); diff --git a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl index 2189a0f618..7323f7d237 100644 --- a/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/sm6_6.descriptorheap.ext.array-stride.accelstruct.hlsl @@ -1,51 +1,52 @@ -// Verifies the descriptor-heap array stride when a RaytracingAccelerationStructure -// is present in the resource heap. +// Two test paths share this file; select via -D RT_STAGE: // -// When acceleration structures are accessed from ResourceDescriptorHeap, the -// stride formula expands from max(sizeof(image), sizeof(buffer)) to the -// three-way max: max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)). -// All resource runtime arrays must share this wider stride so that any slot -// can hold any descriptor type. +// Path A; Condition 1 (RT stage): +// -T lib_6_6 -D RT_STAGE -fspv-extension=SPV_KHR_ray_tracing +// A closesthit entry point is in the workQueue; shaderModelKindIsRayTracing() +// returns true and noteResourceHeapHasAccelStruct() is called unconditionally. // -// The compiler widens the stride under two conditions: +// Path B; Condition 3 (explicit KHR_ray_query extension): +// -T cs_6_6 -E main -fspv-extension=SPV_KHR_ray_query +// No RT stage in workQueue, so Condition 1 is skipped. The +// !spirvOptions.allowedExtensions.empty() guard passes (user listed an +// explicit extension), isExtensionEnabled(KHR_ray_query) is true, and +// noteResourceHeapHasAccelStruct() is called. // -// Path A (RT stage): the shader model is a ray-tracing stage, so any heap -// access may load an acceleration structure. Ray-tracing extensions are -// requested explicitly via -fspv-extension=SPV_KHR_ray_tracing. -// -// Path B (ray query): the SPV_KHR_ray_query extension is explicitly requested -// by the user, signalling that AS descriptors may appear in the heap even -// without a ray-tracing stage. +// Both paths verify that the shared resource-heap array stride expands to +// max(max(sizeof(image), sizeof(buffer)), sizeof(accel_struct)) +// and that ALL resource runtime arrays share that three-way max stride. // // Ordering stress test: Texture2D is accessed BEFORE the AS in source order. -// If the stride cache were populated before the AS widens it, the texture -// runtime array would be decorated with the narrower two-way max stride. +// The stride cache must already hold sizeof(accel_struct) when the first +// runtime array type is created so the texture array uses the correct stride. // -// For the no-AS baseline (exactly 3 OpConstantSizeOfEXT: img, buf, sampler) -// see sm6_6.descriptorheap.ext.array-stride.hlsl. +// The regression test for the "default-extension-mode" false positive (no +// -fspv-extension flags -> allowedExtensions is empty -> Condition 3 is skipped) +// is sm6_6.descriptorheap.ext.array-stride.hlsl, which must emit exactly 3 +// OpConstantSizeOfEXT (img, buf, sampler - no accel_struct). -// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefixes=CHECK,RT -// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_query \ // RUN: -spirv %s | FileCheck %s -// RUN: %dxc -T lib_6_6 -D RT_STAGE -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T lib_6_6 -D RT_STAGE -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \ // RUN: -fspv-extension=SPV_KHR_ray_tracing \ // RUN: -spirv %s | FileCheck %s --check-prefix=SZRT -// RUN: %dxc -T cs_6_6 -E main -Od -fspv-use-descriptor-heap \ +// RUN: %dxc -T cs_6_6 -E main -fspv-use-descriptor-heap \ // RUN: -fspv-target-env=vulkan1.3 \ // RUN: -fspv-extension=SPV_EXT_descriptor_heap \ // RUN: -fspv-extension=SPV_KHR_untyped_pointers \