[llvm] [HLSL][SPIRV] Legalize reinterpretation ptrcasts for byte-addressable buffers (PR #212999)

Tim Corringham via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 30 05:33:36 PDT 2026


https://github.com/tcorringham updated https://github.com/llvm/llvm-project/pull/212999

>From cad727c778d4243ec39da1b83f9ab17b8556a9af Mon Sep 17 00:00:00 2001
From: Tim Corringham <tcorring at amd.com>
Date: Thu, 30 Jul 2026 11:25:37 +0100
Subject: [PATCH 1/3] [HLSL][SPIRV] Legalize reinterpretation ptrcasts for
 byte-addressable buffers

When SPIR-V emit-intrinsics inserts an i8->T spv_ptrcast for typed
loads/stores through byte-addressable resource pointers, legalize the
access directly on the original pointer instead of asserting.

Fixes Clang/Vulkan compilation of RWByteAddressBuffer and local resource
array tests (issue #192523).

Co-authored-by: Cursor
---
 .../Target/SPIRV/SPIRVLegalizePointerCast.cpp | 71 ++++++++++++++++---
 .../SPIRV/passes/SPIRVLegalizePointerCast.ll  | 31 ++++++++
 .../pointers/byte-address-buffer-store.ll     | 17 +++++
 3 files changed, 111 insertions(+), 8 deletions(-)
 create mode 100644 llvm/test/CodeGen/SPIRV/pointers/byte-address-buffer-store.ll

diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizePointerCast.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizePointerCast.cpp
index 847eda7880c03..c332666bc86fb 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizePointerCast.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizePointerCast.cpp
@@ -222,15 +222,59 @@ class SPIRVLegalizePointerCastImpl {
     return std::make_pair(GEP, CurrentTy);
   }
 
+  // Returns the pointer operand of a load/store that uses a ptrcast.
+  static Value *getCastedPointerOperand(Instruction *MemI) {
+    if (auto *LI = dyn_cast<LoadInst>(MemI))
+      return LI->getPointerOperand();
+    if (auto *SI = dyn_cast<StoreInst>(MemI))
+      return SI->getPointerOperand();
+    if (auto *II = dyn_cast<IntrinsicInst>(MemI))
+      if (II->getIntrinsicID() == Intrinsic::spv_store)
+        return II->getArgOperand(1);
+    llvm_unreachable("Unexpected memory instruction being legalized.");
+  }
+
+  // Legalizes a reinterpretation ptrcast: the casted pointer's pointee type
+  // matches the access type but differs from the original pointer layout (e.g.
+  // byte-addressable i8 storage accessed as i32).
+  template <typename MemInstTy>
+  bool tryDirectReinterpretAccess(IRBuilder<> &B, Type *AccessTy,
+                                  Value *OriginalPtr, Value *CastedPtr,
+                                  Align Alignment, bool IsStore,
+                                  Value *StoreSrc, MemInstTy *BadMem) {
+    Type *CastedElemTy = GR->findDeducedElementType(CastedPtr);
+    if (!CastedElemTy || CastedElemTy != AccessTy)
+      return false;
+
+    GR->buildAssignPtr(B, CastedElemTy, OriginalPtr);
+    if (IsStore) {
+      StoreInst *SI = B.CreateStore(StoreSrc, OriginalPtr);
+      SI->setAlignment(Alignment);
+    } else {
+      LoadInst *LI = B.CreateLoad(AccessTy, OriginalPtr);
+      LI->setAlignment(Alignment);
+      buildAssignType(B, AccessTy, LI);
+      GR->replaceAllUsesWith(BadMem, LI, /* DeleteOld= */ true);
+      DeadInstructions.push_back(BadMem);
+    }
+    return true;
+  }
+
   // Builds a legalized load from a pointer, drilling down through
   // memory layouts to find a compatible type. Load flags will be
   // copied from |BadLoad|, which should be the load being legalized.
   Value *buildLegalizedLoad(IRBuilder<> &B, Type *ElementType, Value *Source,
-                            LoadInst *BadLoad) {
+                            LoadInst *BadLoad, Value *CastedPtr) {
     auto ResultOpt = getPointerToFirstCompatibleType(
         B, Source, BadLoad->getPointerOperandType(), ElementType, false);
-    assert(ResultOpt && "Failed to load from aggregate: "
-                        "Could not find compatible memory layout.");
+    if (!ResultOpt) {
+      if (tryDirectReinterpretAccess(
+              B, ElementType, Source, CastedPtr, BadLoad->getAlign(),
+              /*IsStore=*/false, /*StoreSrc=*/nullptr, BadLoad))
+        return nullptr;
+      llvm_unreachable("Failed to load from aggregate: "
+                       "Could not find compatible memory layout.");
+    }
     auto [GEP, CurrentTy] = *ResultOpt;
 
     auto *SAT = dyn_cast<ArrayType>(CurrentTy);
@@ -415,7 +459,10 @@ class SPIRVLegalizePointerCastImpl {
     Type *ToTy = GR->findDeducedElementType(CastedOperand);
     B.SetInsertPoint(LI);
 
-    Value *Output = buildLegalizedLoad(B, ToTy, OriginalOperand, LI);
+    Value *Output =
+        buildLegalizedLoad(B, ToTy, OriginalOperand, LI, CastedOperand);
+    if (!Output)
+      return;
 
     GR->replaceAllUsesWith(LI, Output, /* DeleteOld= */ true);
     DeadInstructions.push_back(LI);
@@ -513,11 +560,18 @@ class SPIRVLegalizePointerCastImpl {
   // Builds a legalized store to a pointer, drilling down through
   // memory layouts to find a compatible type.
   void buildLegalizedStore(IRBuilder<> &B, Value *Src, Value *Dst,
-                           Align Alignment) {
+                           Align Alignment, Value *CastedPtr,
+                           Instruction *BadStore) {
     auto ResultOpt = getPointerToFirstCompatibleType(B, Dst, Dst->getType(),
                                                      Src->getType(), true);
-    assert(ResultOpt && "Failed to store to aggregate: "
-                        "Could not find compatible memory layout.");
+    if (!ResultOpt) {
+      if (tryDirectReinterpretAccess(B, Src->getType(), Dst, CastedPtr,
+                                     Alignment,
+                                     /*IsStore=*/true, Src, BadStore))
+        return;
+      llvm_unreachable("Failed to store to aggregate: "
+                       "Could not find compatible memory layout.");
+    }
     auto [GEP, CurrentTy] = *ResultOpt;
 
     auto *DAT = dyn_cast<ArrayType>(CurrentTy);
@@ -552,7 +606,8 @@ class SPIRVLegalizePointerCastImpl {
   void transformStore(IRBuilder<> &B, Instruction *BadStore, Value *Src,
                       Value *Dst, Align Alignment) {
     B.SetInsertPoint(BadStore);
-    buildLegalizedStore(B, Src, Dst, Alignment);
+    buildLegalizedStore(B, Src, Dst, Alignment,
+                        getCastedPointerOperand(BadStore), BadStore);
     DeadInstructions.push_back(BadStore);
   }
 
diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
index a03dc58c5a5b6..4b988807d0062 100644
--- a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
@@ -76,3 +76,34 @@ entry:
 }
 
 attributes #0 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }
+
+ at .str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+declare target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32, i32, i32, i32, ptr)
+declare ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0), i32)
+
+; Byte-addressable VulkanBuffer getpointer is typed as i8, but typed
+; scalar loads/stores add an i8->T spv_ptrcast. Legalize that by storing
+; directly through the getpointer result.
+
+define spir_func void @byteBufferStore() #0 {
+; CHECK-LABEL: define spir_func void @byteBufferStore(
+; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
+; CHECK: store i32 42, ptr addrspace(11)
+entry:
+  %handle = tail call target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+  %ptr = call ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0) %handle, i32 0)
+  store i32 42, ptr addrspace(11) %ptr, align 4
+  ret void
+}
+
+define spir_func void @byteBufferLoad() #0 {
+; CHECK-LABEL: define spir_func void @byteBufferLoad(
+; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
+; CHECK: load i32, ptr addrspace(11)
+entry:
+  %handle = tail call target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+  %ptr = call ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0) %handle, i32 0)
+  %val = load i32, ptr addrspace(11) %ptr, align 4
+  ret void
+}
diff --git a/llvm/test/CodeGen/SPIRV/pointers/byte-address-buffer-store.ll b/llvm/test/CodeGen/SPIRV/pointers/byte-address-buffer-store.ll
new file mode 100644
index 0000000000000..6eaba6f548619
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/pointers/byte-address-buffer-store.ll
@@ -0,0 +1,17 @@
+; RUN: llc -verify-machineinstrs -O0 -mtriple=spirv-unknown-vulkan-compute %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-unknown-vulkan-compute %s -o - -filetype=obj | spirv-val %}
+
+; CHECK: OpAccessChain
+; CHECK: OpStore
+
+ at .str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+define void @main() local_unnamed_addr #0 {
+entry:
+  %handle = tail call target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+  %ptr = tail call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0) %handle, i32 0)
+  store i32 42, ptr addrspace(11) %ptr, align 4
+  ret void
+}
+
+attributes #0 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }

>From c9965ac714d769dc37a0bb40a9e7f9a0ac14bd90 Mon Sep 17 00:00:00 2001
From: Tim Corringham <tcorring at amd.com>
Date: Thu, 30 Jul 2026 12:52:34 +0100
Subject: [PATCH 2/3] Extend test for issue #192523

Extended the test coverage to include the same def-use chain as seen in
the failure case reported in #192523.
---
 .../SPIRV/passes/SPIRVLegalizePointerCast.ll  | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
index 4b988807d0062..4a34d82ddd812 100644
--- a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
@@ -107,3 +107,22 @@ entry:
   %val = load i32, ptr addrspace(11) %ptr, align 4
   ret void
 }
+
+ at slot = internal global target("spirv.VulkanBuffer", [0 x i8], 12, 0) poison, align 8
+
+; Regression for issue #192523: handle flows through memory before getpointer
+; (as in local RWByteAddressBuffer arrays), then typed store hits i8->i32 ptrcast.
+
+define spir_func void @byteBufferStoreViaLoadedHandle() #0 {
+; CHECK-LABEL: define spir_func void @byteBufferStoreViaLoadedHandle(
+; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
+; CHECK: load target("spirv.VulkanBuffer", [0 x i8], 12, 0), ptr @slot
+; CHECK: store i32 42, ptr addrspace(11)
+entry:
+  %handle = tail call target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+  store target("spirv.VulkanBuffer", [0 x i8], 12, 0) %handle, ptr @slot, align 8
+  %loaded = load target("spirv.VulkanBuffer", [0 x i8], 12, 0), ptr @slot, align 8
+  %ptr = call ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0) %loaded, i32 0)
+  store i32 42, ptr addrspace(11) %ptr, align 4
+  ret void
+}

>From ca6543bec24c542026570edbc8acb66f757c4e70 Mon Sep 17 00:00:00 2001
From: Tim Corringham <tcorring at amd.com>
Date: Thu, 30 Jul 2026 13:19:06 +0100
Subject: [PATCH 3/3] [SPIRV] Clarify and extend byte-buffer ptrcast
 legalization tests

Document that [0 x i8] is a storage-buffer layout type, not an HLSL surface
type. Drop unneeded HLSL function attributes from the byte-buffer cases and
add a loaded-handle load regression alongside the existing store path.

Co-authored-by: Cursor
---
 .../SPIRV/passes/SPIRVLegalizePointerCast.ll  | 39 ++++++++++++++-----
 1 file changed, 29 insertions(+), 10 deletions(-)

diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
index 4a34d82ddd812..706b1ef2c4dcb 100644
--- a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizePointerCast.ll
@@ -82,12 +82,17 @@ attributes #0 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }
 declare target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32, i32, i32, i32, ptr)
 declare ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0), i32)
 
-; Byte-addressable VulkanBuffer getpointer is typed as i8, but typed
-; scalar loads/stores add an i8->T spv_ptrcast. Legalize that by storing
-; directly through the getpointer result.
-
-define spir_func void @byteBufferStore() #0 {
-; CHECK-LABEL: define spir_func void @byteBufferStore(
+; Byte-addressable buffer tests model Clang's SPIR-V resource layout for HLSL
+; ByteAddressBuffer/RWByteAddressBuffer: the handle carries [0 x i8] as the
+; OpTypeRuntimeArray element type for storage-buffer blocks. That i8 names
+; byte-addressable layout in LLVM IR; it is not an HLSL surface type. Typed
+; HLSL accesses (e.g. Store(offset, uint)) lower to i32 load/store at a byte
+; offset in getpointer's index operand. emit-intrinsics tags getpointer as i8
+; from the layout, then inserts i8->T spv_ptrcast for the typed access;
+; legalize-pointer-cast removes the ptrcast and keeps the typed access.
+
+define void @byteBufferStore() {
+; CHECK-LABEL: define void @byteBufferStore(
 ; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
 ; CHECK: store i32 42, ptr addrspace(11)
 entry:
@@ -97,8 +102,8 @@ entry:
   ret void
 }
 
-define spir_func void @byteBufferLoad() #0 {
-; CHECK-LABEL: define spir_func void @byteBufferLoad(
+define void @byteBufferLoad() {
+; CHECK-LABEL: define void @byteBufferLoad(
 ; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
 ; CHECK: load i32, ptr addrspace(11)
 entry:
@@ -113,8 +118,8 @@ entry:
 ; Regression for issue #192523: handle flows through memory before getpointer
 ; (as in local RWByteAddressBuffer arrays), then typed store hits i8->i32 ptrcast.
 
-define spir_func void @byteBufferStoreViaLoadedHandle() #0 {
-; CHECK-LABEL: define spir_func void @byteBufferStoreViaLoadedHandle(
+define void @byteBufferStoreViaLoadedHandle() {
+; CHECK-LABEL: define void @byteBufferStoreViaLoadedHandle(
 ; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
 ; CHECK: load target("spirv.VulkanBuffer", [0 x i8], 12, 0), ptr @slot
 ; CHECK: store i32 42, ptr addrspace(11)
@@ -126,3 +131,17 @@ entry:
   store i32 42, ptr addrspace(11) %ptr, align 4
   ret void
 }
+
+define void @byteBufferLoadViaLoadedHandle() {
+; CHECK-LABEL: define void @byteBufferLoadViaLoadedHandle(
+; CHECK-NOT: call {{.*}}@llvm.spv.ptrcast
+; CHECK: load target("spirv.VulkanBuffer", [0 x i8], 12, 0), ptr @slot
+; CHECK: load i32, ptr addrspace(11)
+entry:
+  %handle = tail call target("spirv.VulkanBuffer", [0 x i8], 12, 0) @llvm.spv.resource.handlefrombinding(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+  store target("spirv.VulkanBuffer", [0 x i8], 12, 0) %handle, ptr @slot, align 8
+  %loaded = load target("spirv.VulkanBuffer", [0 x i8], 12, 0), ptr @slot, align 8
+  %ptr = call ptr addrspace(11) @llvm.spv.resource.getpointer(target("spirv.VulkanBuffer", [0 x i8], 12, 0) %loaded, i32 0)
+  %val = load i32, ptr addrspace(11) %ptr, align 4
+  ret void
+}



More information about the llvm-commits mailing list