[clang] [llvm] [HLSL] Add Texture2DMS (PR #211972)

Deric C. via llvm-commits llvm-commits at lists.llvm.org
Tue Aug 18 18:57:32 PDT 2026


https://github.com/Icohedron updated https://github.com/llvm/llvm-project/pull/211972

>From 676c00bafc02a2ef6eece6b6a596f1bfa77d0246 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Thu, 23 Jul 2026 17:49:44 -0700
Subject: [PATCH 1/6] [HLSL] Add Texture2DMS

Declares Texture2DMS<element_type, int sample_count = 0> with
Load(location, sampleIndex[, offset]) and operator[], plus the
resource.load.ms intrinsic for DirectX and SPIR-V.

The sample count is a non-type template parameter, so the resource handle
type now takes part in template instantiation, and lowers to the
dx.MSTexture sample-count operand. A sample count of 0 means the count
comes from the bound resource at runtime.
---
 clang/include/clang/AST/ASTContext.h          |   3 +-
 clang/include/clang/AST/TypeBase.h            |  42 +++----
 clang/include/clang/AST/TypeProperties.td     |   6 +-
 clang/include/clang/Basic/Builtins.td         |   6 +
 clang/include/clang/Sema/SemaHLSL.h           |   3 +-
 clang/lib/AST/ASTContext.cpp                  |   3 +-
 clang/lib/AST/ASTImporter.cpp                 |   3 +-
 clang/lib/AST/Type.cpp                        |  36 ++++++
 clang/lib/CodeGen/CGHLSLBuiltins.cpp          |  19 +++
 clang/lib/CodeGen/CGHLSLRuntime.h             |   1 +
 clang/lib/CodeGen/Targets/DirectX.cpp         |  29 ++++-
 clang/lib/CodeGen/Targets/SPIR.cpp            |   2 +-
 clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp | 119 ++++++++++++++++--
 clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h   |  12 +-
 clang/lib/Sema/HLSLExternalSemaSource.cpp     |  31 +++++
 clang/lib/Sema/SemaHLSL.cpp                   |  49 +++++++-
 clang/lib/Sema/TreeTransform.h                |  16 ++-
 .../AST/HLSL/MultiSampledTextures-AST.hlsl    |  97 ++++++++++++++
 .../resources/MultiSampledTextures-Load.hlsl  |  64 ++++++++++
 ...pledTextures-default-explicit-binding.hlsl |  31 +++++
 .../MultiSampledTextures-default.hlsl         |  27 ++++
 .../resources/MultiSampledTextures-pch.hlsl   |  23 ++++
 .../resources/Textures-Subscript.hlsl         |  55 ++++----
 .../Resources/MultiSampledTextures-Sema.hlsl  |  55 ++++++++
 llvm/include/llvm/IR/IntrinsicsDirectX.td     |   6 +
 llvm/include/llvm/IR/IntrinsicsSPIRV.td       |   6 +
 26 files changed, 669 insertions(+), 75 deletions(-)
 create mode 100644 clang/test/AST/HLSL/MultiSampledTextures-AST.hlsl
 create mode 100644 clang/test/CodeGenHLSL/resources/MultiSampledTextures-Load.hlsl
 create mode 100644 clang/test/CodeGenHLSL/resources/MultiSampledTextures-default-explicit-binding.hlsl
 create mode 100644 clang/test/CodeGenHLSL/resources/MultiSampledTextures-default.hlsl
 create mode 100644 clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl
 create mode 100644 clang/test/SemaHLSL/Resources/MultiSampledTextures-Sema.hlsl

diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h
index 7ed6509c3c16c..5a4a0840c7919 100644
--- a/clang/include/clang/AST/ASTContext.h
+++ b/clang/include/clang/AST/ASTContext.h
@@ -296,7 +296,8 @@ class ASTContext : public RefCountedBase<ASTContext> {
       DependentBitIntTypes;
   mutable llvm::FoldingSet<BTFTagAttributedType> BTFTagAttributedTypes;
   mutable llvm::FoldingSet<OverflowBehaviorType> OverflowBehaviorTypes;
-  llvm::FoldingSet<HLSLAttributedResourceType> HLSLAttributedResourceTypes;
+  mutable llvm::ContextualFoldingSet<HLSLAttributedResourceType, ASTContext &>
+      HLSLAttributedResourceTypes;
   llvm::FoldingSet<HLSLInlineSpirvType> HLSLInlineSpirvTypes;
 
   mutable llvm::FoldingSet<CountAttributedType> CountAttributedTypes;
diff --git a/clang/include/clang/AST/TypeBase.h b/clang/include/clang/AST/TypeBase.h
index 00b6f1b20ad8e..370f129ce02ac 100644
--- a/clang/include/clang/AST/TypeBase.h
+++ b/clang/include/clang/AST/TypeBase.h
@@ -6850,14 +6850,18 @@ class HLSLAttributedResourceType : public Type, public llvm::FoldingSetNode {
     LLVM_PREFERRED_TYPE(bool)
     uint8_t IsMultiSampled : 1;
 
+    /// The N in Texture2DMS<T, N>; null for every other resource.
+    Expr *SampleCountExpr;
+
     Attributes(llvm::dxil::ResourceClass ResourceClass,
                llvm::dxil::ResourceDimension ResourceDimension,
                bool IsROV = false, bool RawBuffer = false,
                bool IsCounter = false, bool IsArray = false,
-               bool IsMultiSampled = false)
+               bool IsMultiSampled = false, Expr *SampleCountExpr = nullptr)
         : ResourceClass(ResourceClass), ResourceDimension(ResourceDimension),
           IsROV(IsROV), RawBuffer(RawBuffer), IsCounter(IsCounter),
-          IsArray(IsArray), IsMultiSampled(IsMultiSampled) {}
+          IsArray(IsArray), IsMultiSampled(IsMultiSampled),
+          SampleCountExpr(SampleCountExpr) {}
 
     Attributes(llvm::dxil::ResourceClass ResourceClass)
         : Attributes(ResourceClass, llvm::dxil::ResourceDimension::Unknown) {}
@@ -6870,10 +6874,10 @@ class HLSLAttributedResourceType : public Type, public llvm::FoldingSetNode {
     friend bool operator==(const Attributes &LHS, const Attributes &RHS) {
       return std::tie(LHS.ResourceClass, LHS.ResourceDimension, LHS.IsROV,
                       LHS.RawBuffer, LHS.IsCounter, LHS.IsArray,
-                      LHS.IsMultiSampled) ==
+                      LHS.IsMultiSampled, LHS.SampleCountExpr) ==
              std::tie(RHS.ResourceClass, RHS.ResourceDimension, RHS.IsROV,
                       RHS.RawBuffer, RHS.IsCounter, RHS.IsArray,
-                      RHS.IsMultiSampled);
+                      RHS.IsMultiSampled, RHS.SampleCountExpr);
     }
     friend bool operator!=(const Attributes &LHS, const Attributes &RHS) {
       return !(LHS == RHS);
@@ -6888,16 +6892,17 @@ class HLSLAttributedResourceType : public Type, public llvm::FoldingSetNode {
   const Attributes Attrs;
 
   HLSLAttributedResourceType(QualType Wrapped, QualType Contained,
-                             const Attributes &Attrs)
-      : Type(HLSLAttributedResource, QualType(),
-             Contained.isNull() ? TypeDependence::None
-                                : Contained->getDependence()),
-        WrappedType(Wrapped), ContainedType(Contained), Attrs(Attrs) {}
+                             const Attributes &Attrs);
+
+  /// WrappedType is always __hlsl_resource_t, so it never contributes.
+  static TypeDependence computeDependence(QualType Contained,
+                                          const Attributes &Attrs);
 
 public:
   QualType getWrappedType() const { return WrappedType; }
   QualType getContainedType() const { return ContainedType; }
   bool hasContainedType() const { return !ContainedType.isNull(); }
+  Expr *getSampleCountExpr() const { return Attrs.SampleCountExpr; }
   const Attributes &getAttrs() const { return Attrs; }
   bool isRaw() const { return Attrs.RawBuffer; }
   bool isStructured() const { return !ContainedType->isChar8Type(); }
@@ -6905,22 +6910,13 @@ class HLSLAttributedResourceType : public Type, public llvm::FoldingSetNode {
   bool isSugared() const { return false; }
   QualType desugar() const { return QualType(this, 0); }
 
-  void Profile(llvm::FoldingSetNodeID &ID) {
-    Profile(ID, WrappedType, ContainedType, Attrs);
+  void Profile(llvm::FoldingSetNodeID &ID, const ASTContext &Ctx) {
+    Profile(ID, Ctx, WrappedType, ContainedType, Attrs);
   }
 
-  static void Profile(llvm::FoldingSetNodeID &ID, QualType Wrapped,
-                      QualType Contained, const Attributes &Attrs) {
-    ID.AddPointer(Wrapped.getAsOpaquePtr());
-    ID.AddPointer(Contained.getAsOpaquePtr());
-    ID.AddInteger(static_cast<uint32_t>(Attrs.ResourceClass));
-    ID.AddInteger(static_cast<uint32_t>(Attrs.ResourceDimension));
-    ID.AddBoolean(Attrs.IsROV);
-    ID.AddBoolean(Attrs.RawBuffer);
-    ID.AddBoolean(Attrs.IsCounter);
-    ID.AddBoolean(Attrs.IsArray);
-    ID.AddBoolean(Attrs.IsMultiSampled);
-  }
+  static void Profile(llvm::FoldingSetNodeID &ID, const ASTContext &Ctx,
+                      QualType Wrapped, QualType Contained,
+                      const Attributes &Attrs);
 
   static bool classof(const Type *T) {
     return T->getTypeClass() == HLSLAttributedResource;
diff --git a/clang/include/clang/AST/TypeProperties.td b/clang/include/clang/AST/TypeProperties.td
index f202b889286bf..97eacae1a80b7 100644
--- a/clang/include/clang/AST/TypeProperties.td
+++ b/clang/include/clang/AST/TypeProperties.td
@@ -690,11 +690,15 @@ let Class = HLSLAttributedResourceType in {
   def : Property<"isMultiSampled", Bool> {
     let Read = [{ node->getAttrs().IsMultiSampled }];
   }
+  def : Property<"sampleCountExpr", Optional<ExprRef>> {
+    let Read = [{ makeOptionalFromPointer(node->getSampleCountExpr()) }];
+  }
   def : Creator<[{
     HLSLAttributedResourceType::Attributes attrs(
         static_cast<llvm::dxil::ResourceClass>(resClass),
         static_cast<llvm::dxil::ResourceDimension>(resDimension), isROV,
-        rawBuffer, isCounter, isArray, isMultiSampled);
+        rawBuffer, isCounter, isArray, isMultiSampled,
+        makePointerFromOptional(sampleCountExpr));
     return ctx.getHLSLAttributedResourceType(wrappedTy, containedTy, attrs);
   }]>;
 }
diff --git a/clang/include/clang/Basic/Builtins.td b/clang/include/clang/Basic/Builtins.td
index 344a712ddc585..3239938aff9b3 100644
--- a/clang/include/clang/Basic/Builtins.td
+++ b/clang/include/clang/Basic/Builtins.td
@@ -5335,6 +5335,12 @@ def HLSLResourceLoadLevel : LangBuiltin<"HLSL_LANG"> {
   let Prototype = "void(...)";
 }
 
+def HLSLResourceLoadMS : LangBuiltin<"HLSL_LANG"> {
+  let Spellings = ["__builtin_hlsl_resource_load_ms"];
+  let Attributes = [NoThrow];
+  let Prototype = "void(...)";
+}
+
 def HLSLResourceSample : LangBuiltin<"HLSL_LANG"> {
   let Spellings = ["__builtin_hlsl_resource_sample"];
   let Attributes = [NoThrow];
diff --git a/clang/include/clang/Sema/SemaHLSL.h b/clang/include/clang/Sema/SemaHLSL.h
index 8928524e49783..fd9df822cd632 100644
--- a/clang/include/clang/Sema/SemaHLSL.h
+++ b/clang/include/clang/Sema/SemaHLSL.h
@@ -60,7 +60,8 @@ using llvm::dxil::ResourceClass;
 // longer need to create builtin buffer types in HLSLExternalSemaSource.
 bool CreateHLSLAttributedResourceType(
     Sema &S, QualType Wrapped, ArrayRef<const Attr *> AttrList,
-    QualType &ResType, HLSLAttributedResourceLocInfo *LocInfo = nullptr);
+    QualType &ResType, HLSLAttributedResourceLocInfo *LocInfo = nullptr,
+    Expr *SampleCountExpr = nullptr);
 
 enum class BindingType : uint8_t { NotAssigned, Explicit, Implicit };
 
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index 2228811546c0f..a5b775a60f401 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -938,6 +938,7 @@ ASTContext::ASTContext(LangOptions &LOpts, SourceManager &SM,
       DependentTypeOfExprTypes(this_()), DependentDecltypeTypes(this_()),
       DependentPackIndexingTypes(this_()), TemplateSpecializationTypes(this_()),
       AttributedTypes(this_()), DependentBitIntTypes(this_()),
+      HLSLAttributedResourceTypes(this_()),
       SubstTemplateTemplateParmPacks(this_()), DeducedTemplates(this_()),
       ArrayParameterTypes(this_()), CanonTemplateTemplateParms(this_()),
       SourceMgr(SM), LangOpts(LOpts),
@@ -5874,7 +5875,7 @@ QualType ASTContext::getHLSLAttributedResourceType(
     const HLSLAttributedResourceType::Attributes &Attrs) {
 
   llvm::FoldingSetNodeID ID;
-  HLSLAttributedResourceType::Profile(ID, Wrapped, Contained, Attrs);
+  HLSLAttributedResourceType::Profile(ID, *this, Wrapped, Contained, Attrs);
 
   void *InsertPos = nullptr;
   HLSLAttributedResourceType *Ty =
diff --git a/clang/lib/AST/ASTImporter.cpp b/clang/lib/AST/ASTImporter.cpp
index db7d223d56af3..8c0c314df97f0 100644
--- a/clang/lib/AST/ASTImporter.cpp
+++ b/clang/lib/AST/ASTImporter.cpp
@@ -2036,9 +2036,10 @@ ExpectedType clang::ASTNodeImporter::VisitOverflowBehaviorType(
 ExpectedType clang::ASTNodeImporter::VisitHLSLAttributedResourceType(
     const clang::HLSLAttributedResourceType *T) {
   Error Err = Error::success();
-  const HLSLAttributedResourceType::Attributes &ToAttrs = T->getAttrs();
+  HLSLAttributedResourceType::Attributes ToAttrs = T->getAttrs();
   QualType ToWrappedType = importChecked(Err, T->getWrappedType());
   QualType ToContainedType = importChecked(Err, T->getContainedType());
+  ToAttrs.SampleCountExpr = importChecked(Err, T->getSampleCountExpr());
   if (Err)
     return std::move(Err);
 
diff --git a/clang/lib/AST/Type.cpp b/clang/lib/AST/Type.cpp
index 42d148715bc40..db3c94e13b7bc 100644
--- a/clang/lib/AST/Type.cpp
+++ b/clang/lib/AST/Type.cpp
@@ -5952,6 +5952,42 @@ std::string FunctionEffectWithCondition::description() const {
   return Result;
 }
 
+TypeDependence
+HLSLAttributedResourceType::computeDependence(QualType Contained,
+                                              const Attributes &Attrs) {
+  TypeDependence Deps = TypeDependence::None;
+  if (!Contained.isNull())
+    Deps |= Contained->getDependence();
+  if (Attrs.SampleCountExpr)
+    Deps |= toTypeDependence(Attrs.SampleCountExpr->getDependence());
+  return Deps;
+}
+
+HLSLAttributedResourceType::HLSLAttributedResourceType(QualType Wrapped,
+                                                       QualType Contained,
+                                                       const Attributes &Attrs)
+    : Type(HLSLAttributedResource, QualType(),
+           computeDependence(Contained, Attrs)),
+      WrappedType(Wrapped), ContainedType(Contained), Attrs(Attrs) {}
+
+void HLSLAttributedResourceType::Profile(llvm::FoldingSetNodeID &ID,
+                                         const ASTContext &Ctx,
+                                         QualType Wrapped, QualType Contained,
+                                         const Attributes &Attrs) {
+  ID.AddPointer(Wrapped.getAsOpaquePtr());
+  ID.AddPointer(Contained.getAsOpaquePtr());
+  ID.AddInteger(static_cast<uint32_t>(Attrs.ResourceClass));
+  ID.AddInteger(static_cast<uint32_t>(Attrs.ResourceDimension));
+  ID.AddBoolean(Attrs.IsROV);
+  ID.AddBoolean(Attrs.RawBuffer);
+  ID.AddBoolean(Attrs.IsCounter);
+  ID.AddBoolean(Attrs.IsArray);
+  ID.AddBoolean(Attrs.IsMultiSampled);
+  ID.AddBoolean(Attrs.SampleCountExpr != nullptr);
+  if (Attrs.SampleCountExpr)
+    Attrs.SampleCountExpr->Profile(ID, Ctx, /*Canonical=*/true);
+}
+
 const HLSLAttributedResourceType *
 HLSLAttributedResourceType::findHandleTypeOnResource(const Type *RT) {
   // If the type RT is an HLSL resource class, the first field must
diff --git a/clang/lib/CodeGen/CGHLSLBuiltins.cpp b/clang/lib/CodeGen/CGHLSLBuiltins.cpp
index 1bda113143e07..86f2c3be1d065 100644
--- a/clang/lib/CodeGen/CGHLSLBuiltins.cpp
+++ b/clang/lib/CodeGen/CGHLSLBuiltins.cpp
@@ -776,6 +776,25 @@ Value *CodeGenFunction::EmitHLSLBuiltinExpr(unsigned BuiltinID,
     return Builder.CreateIntrinsic(
         RetTy, CGM.getHLSLRuntime().getLoadLevelIntrinsic(), Args);
   }
+  case Builtin::BI__builtin_hlsl_resource_load_ms: {
+    Value *HandleOp = EmitScalarExpr(E->getArg(0));
+    Value *CoordOp = EmitScalarExpr(E->getArg(1));
+    Value *SampleOp = EmitScalarExpr(E->getArg(2));
+    if (SampleOp->getType() != Builder.getInt32Ty())
+      SampleOp = Builder.CreateIntCast(SampleOp, Builder.getInt32Ty(),
+                                       /*isSigned=*/true);
+    const HLSLAttributedResourceType *RT = getRequiredHandleType(E, 0);
+
+    SmallVector<Value *, 4> Args;
+    Args.push_back(HandleOp);
+    Args.push_back(CoordOp);
+    Args.push_back(SampleOp);
+    Args.push_back(emitHlslOffset(*this, E, 3, getOffsetType(CGM, RT)));
+
+    llvm::Type *RetTy = ConvertType(E->getType());
+    return Builder.CreateIntrinsic(
+        RetTy, CGM.getHLSLRuntime().getLoadMSIntrinsic(), Args);
+  }
   case Builtin::BI__builtin_hlsl_resource_sample_cmp: {
     Value *HandleOp = EmitScalarExpr(E->getArg(0));
     Value *SamplerOp = EmitScalarExpr(E->getArg(1));
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.h b/clang/lib/CodeGen/CGHLSLRuntime.h
index 0664eef464f98..4b8e4b6a96a24 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.h
+++ b/clang/lib/CodeGen/CGHLSLRuntime.h
@@ -210,6 +210,7 @@ class CGHLSLRuntime {
   GENERATE_HLSL_INTRINSIC_FUNCTION(GetDimensionsLevelsXY,
                                    resource_getdimensions_levels_xy)
   GENERATE_HLSL_INTRINSIC_FUNCTION(LoadLevel, resource_load_level)
+  GENERATE_HLSL_INTRINSIC_FUNCTION(LoadMS, resource_load_ms)
   GENERATE_HLSL_INTRINSIC_FUNCTION(CalculateLod, resource_calculate_lod)
   GENERATE_HLSL_INTRINSIC_FUNCTION(CalculateLodUnclamped,
                                    resource_calculate_lod_unclamped)
diff --git a/clang/lib/CodeGen/Targets/DirectX.cpp b/clang/lib/CodeGen/Targets/DirectX.cpp
index f483a7b867016..29998f6dfb16f 100644
--- a/clang/lib/CodeGen/Targets/DirectX.cpp
+++ b/clang/lib/CodeGen/Targets/DirectX.cpp
@@ -71,15 +71,30 @@ llvm::Type *DirectXTargetCodeGenInfo::getHLSLType(
         ResAttrs.ResourceDimension != llvm::dxil::ResourceDimension::Unknown;
     assert((!IsRawBuffer || !IsTexture) && "A resource cannot be both a raw "
                                            "buffer and a texture.");
+    bool IsMultiSampledTexture = IsTexture && ResAttrs.IsMultiSampled;
     llvm::StringRef TypeName = "dx.TypedBuffer";
     if (IsRawBuffer)
       TypeName = "dx.RawBuffer";
+    else if (IsMultiSampledTexture)
+      TypeName = "dx.MSTexture";
     else if (IsTexture)
       TypeName = "dx.Texture";
 
-    SmallVector<unsigned, 4> Ints = {/*IsWriteable*/ ResAttrs.ResourceClass ==
-                                         llvm::dxil::ResourceClass::UAV,
-                                     /*IsROV*/ ResAttrs.IsROV};
+    // The second int operand is overloaded: dx.Texture holds IsROV there,
+    // dx.MSTexture holds the sample count. A sample count of 0 means the count
+    // comes from the bound resource at runtime, which is also what a
+    // Texture2DMS<T> written without an explicit N lowers to.
+    unsigned SampleCount = 0;
+    if (const Expr *SCE = ResAttrs.SampleCountExpr) {
+      std::optional<llvm::APSInt> Count =
+          SCE->getIntegerConstantExpr(CGM.getContext());
+      if (Count && Count->isNonNegative())
+        SampleCount = Count->getZExtValue();
+    }
+    SmallVector<unsigned, 4> Ints = {
+        /*IsWriteable*/ ResAttrs.ResourceClass ==
+            llvm::dxil::ResourceClass::UAV,
+        IsMultiSampledTexture ? SampleCount : /*IsROV*/ ResAttrs.IsROV};
     if (!IsRawBuffer) {
       const clang::Type *ElemType = ContainedTy->getUnqualifiedDesugaredType();
       if (ElemType->isVectorType())
@@ -97,10 +112,12 @@ llvm::Type *DirectXTargetCodeGenInfo::getHLSLType(
         RK = llvm::dxil::ResourceKind::Texture1D;
         break;
       case llvm::dxil::ResourceDimension::Dim2D:
-        if (ResAttrs.IsArray)
-          RK = llvm::dxil::ResourceKind::Texture2DArray;
+        if (ResAttrs.IsMultiSampled)
+          RK = ResAttrs.IsArray ? llvm::dxil::ResourceKind::Texture2DMSArray
+                                : llvm::dxil::ResourceKind::Texture2DMS;
         else
-          RK = llvm::dxil::ResourceKind::Texture2D;
+          RK = ResAttrs.IsArray ? llvm::dxil::ResourceKind::Texture2DArray
+                                : llvm::dxil::ResourceKind::Texture2D;
         break;
       case llvm::dxil::ResourceDimension::Dim3D:
         RK = llvm::dxil::ResourceKind::Texture3D;
diff --git a/clang/lib/CodeGen/Targets/SPIR.cpp b/clang/lib/CodeGen/Targets/SPIR.cpp
index c8f7c03474be9..dba7d4fa73923 100644
--- a/clang/lib/CodeGen/Targets/SPIR.cpp
+++ b/clang/lib/CodeGen/Targets/SPIR.cpp
@@ -936,7 +936,7 @@ llvm::Type *CommonSPIRTargetCodeGenInfo::getSPIRVImageTypeFromHLSLResource(
   IntParams[2] = static_cast<unsigned>(attributes.IsArray);
 
   // MS
-  IntParams[3] = 0;
+  IntParams[3] = static_cast<unsigned>(attributes.IsMultiSampled);
 
   // Sampled
   IntParams[4] =
diff --git a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
index f8018729b4644..abe97d34ea72c 100644
--- a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
+++ b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
@@ -110,6 +110,10 @@ struct TemplateParameterListBuilder {
   TemplateParameterListBuilder &
   addTypeParameter(StringRef Name, QualType DefaultValue = QualType());
 
+  TemplateParameterListBuilder &
+  addNonTypeParameter(StringRef Name, QualType Ty,
+                      Expr *DefaultValue = nullptr);
+
   ConceptSpecializationExpr *
   constructConceptSpecializationExpr(Sema &S, ConceptDecl *CD);
 
@@ -295,6 +299,28 @@ TemplateParameterListBuilder::addTypeParameter(StringRef Name,
   return *this;
 }
 
+TemplateParameterListBuilder &
+TemplateParameterListBuilder::addNonTypeParameter(StringRef Name, QualType Ty,
+                                                  Expr *DefaultValue) {
+  assert(!Builder.Record->isCompleteDefinition() &&
+         "record is already complete");
+  ASTContext &AST = Builder.SemaRef.getASTContext();
+  unsigned Position = static_cast<unsigned>(Params.size());
+  auto *Decl = NonTypeTemplateParmDecl::Create(
+      AST, Builder.Record->getDeclContext(), SourceLocation(), SourceLocation(),
+      /* TemplateDepth */ 0, Position,
+      &AST.Idents.get(Name, tok::TokenKind::identifier), Ty,
+      /* ParameterPack */ false, AST.getTrivialTypeSourceInfo(Ty));
+  if (DefaultValue)
+    Decl->setDefaultArgument(
+        AST, Builder.SemaRef.getTrivialTemplateArgumentLoc(
+                 TemplateArgument(DefaultValue, /*IsCanonical=*/false), Ty,
+                 SourceLocation()));
+
+  Params.emplace_back(Decl);
+  return *this;
+}
+
 // The concept specialization expression (CSE) constructed in
 // constructConceptSpecializationExpr is constructed so that it
 // matches the CSE that is constructed when parsing the below C++ code:
@@ -1039,18 +1065,18 @@ BuiltinTypeDeclBuilder::addBufferHandles(ResourceClass RC, bool IsROV,
                                          AccessSpecifier Access) {
   QualType ElementTy = getHandleElementType();
   addHandleMember(RC, ResourceDimension::Unknown, IsROV, RawBuffer,
-                  /*IsArray=*/false, ElementTy, Access);
+                  /*IsArray=*/false, ElementTy, /*IsMultiSampled=*/false,
+                  Access);
   if (HasCounter)
     addCounterHandleMember(RC, IsROV, RawBuffer, ElementTy, Access);
   return *this;
 }
 
-BuiltinTypeDeclBuilder &
-BuiltinTypeDeclBuilder::addTextureHandle(ResourceClass RC, bool IsROV,
-                                         bool IsArray, ResourceDimension RD,
-                                         AccessSpecifier Access) {
+BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addTextureHandle(
+    ResourceClass RC, bool IsROV, bool IsArray, ResourceDimension RD,
+    bool IsMultiSampled, AccessSpecifier Access) {
   addHandleMember(RC, RD, IsROV, /*RawBuffer=*/false, IsArray,
-                  getHandleElementType(), Access);
+                  getHandleElementType(), IsMultiSampled, Access);
   return *this;
 }
 
@@ -1112,9 +1138,11 @@ CXXRecordDecl *BuiltinTypeDeclBuilder::addPrivateNestedRecord(StringRef Name) {
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addHandleMember(
     ResourceClass RC, ResourceDimension RD, bool IsROV, bool RawBuffer,
-    bool IsArray, QualType ElementTy, AccessSpecifier Access) {
+    bool IsArray, QualType ElementTy, bool IsMultiSampled,
+    AccessSpecifier Access) {
   return addResourceMember("__handle", RC, RD, IsROV, RawBuffer,
-                           /*IsCounter=*/false, IsArray, ElementTy, Access);
+                           /*IsCounter=*/false, IsArray, ElementTy,
+                           IsMultiSampled, Access);
 }
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addCounterHandleMember(
@@ -1122,13 +1150,14 @@ BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addCounterHandleMember(
     AccessSpecifier Access) {
   return addResourceMember("__counter_handle", RC, ResourceDimension::Unknown,
                            IsROV, RawBuffer, /*IsCounter=*/true,
-                           /*IsArray=*/false, ElementTy, Access);
+                           /*IsArray=*/false, ElementTy,
+                           /*IsMultiSampled=*/false, Access);
 }
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addResourceMember(
     StringRef MemberName, ResourceClass RC, ResourceDimension RD, bool IsROV,
     bool RawBuffer, bool IsCounter, bool IsArray, QualType ElementTy,
-    AccessSpecifier Access) {
+    bool IsMultiSampled, AccessSpecifier Access) {
   assert(!Record->isCompleteDefinition() && "record is already complete");
 
   ASTContext &Ctx = SemaRef.getASTContext();
@@ -1154,9 +1183,20 @@ BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addResourceMember(
     Attrs.push_back(HLSLIsCounterAttr::CreateImplicit(Ctx));
   if (IsArray)
     Attrs.push_back(HLSLIsArrayAttr::CreateImplicit(Ctx));
+  Expr *SampleCountExpr = nullptr;
+  if (IsMultiSampled) {
+    Attrs.push_back(HLSLIsMultiSampledAttr::CreateImplicit(Ctx));
+    ClassTemplateDecl *CTD = Record->getDescribedClassTemplate();
+    assert(CTD && "multisampled texture must be a class template");
+    auto *NTTP = cast<NonTypeTemplateParmDecl>(
+        CTD->getTemplateParameters()->getParam(1));
+    SampleCountExpr = SemaRef.BuildDeclRefExpr(NTTP, NTTP->getType(),
+                                               VK_PRValue, SourceLocation());
+  }
 
   if (CreateHLSLAttributedResourceType(SemaRef, Ctx.HLSLResourceTy, Attrs,
-                                       AttributedResTy))
+                                       AttributedResTy, /*LocInfo=*/nullptr,
+                                       SampleCountExpr))
     addMemberVariable(MemberName, AttributedResTy, {}, Access);
   return *this;
 }
@@ -1466,7 +1506,7 @@ CXXRecordDecl *BuiltinTypeDeclBuilder::addMipsSliceType(ResourceDimension Dim,
       .addHandleMember(getResourceAttrs().ResourceClass, Dim,
                        getResourceAttrs().IsROV, /*RawBuffer=*/false,
                        getResourceAttrs().IsArray, ReturnType,
-                       AccessSpecifier::AS_public)
+                       /*IsMultiSampled=*/false, AccessSpecifier::AS_public)
       .addMemberVariable("__level", IntTy, {}, AccessSpecifier::AS_public)
       .addDefaultHandleConstructor(AccessSpecifier::AS_protected)
       .addCopyConstructor(AccessSpecifier::AS_protected)
@@ -1510,7 +1550,7 @@ CXXRecordDecl *BuiltinTypeDeclBuilder::addMipsType(ResourceDimension Dim,
       .addHandleMember(getResourceAttrs().ResourceClass, Dim,
                        getResourceAttrs().IsROV, /*RawBuffer=*/false,
                        getResourceAttrs().IsArray, ReturnType,
-                       AccessSpecifier::AS_public)
+                       /*IsMultiSampled=*/false, AccessSpecifier::AS_public)
       .addDefaultHandleConstructor(AccessSpecifier::AS_protected)
       .addCopyConstructor(AccessSpecifier::AS_protected)
       .addCopyAssignmentOperator(AccessSpecifier::AS_protected);
@@ -1589,6 +1629,39 @@ BuiltinTypeDeclBuilder::addTextureLoadMethods(ResourceDimension Dim,
       .finalize();
 }
 
+BuiltinTypeDeclBuilder &
+BuiltinTypeDeclBuilder::addTextureLoadMSMethods(ResourceDimension Dim,
+                                                bool IsArray) {
+  assert(!Record->isCompleteDefinition() && "record is already complete");
+  ASTContext &AST = Record->getASTContext();
+  uint32_t OffsetSize = getResourceDimensions(Dim);
+  // Multisampled textures use a plain location (no mip/LOD component).
+  uint32_t CoordSize = OffsetSize + (IsArray ? 1 : 0);
+  QualType IntTy = AST.IntTy;
+  QualType OffsetTy = AST.getExtVectorType(IntTy, OffsetSize);
+  QualType LocationTy = AST.getExtVectorType(IntTy, CoordSize);
+  QualType ReturnType = getHandleElementType();
+
+  using PH = BuiltinTypeMethodBuilder::PlaceHolder;
+
+  // T Load(int2 location, int sampleIndex)
+  BuiltinTypeMethodBuilder(*this, "Load", ReturnType)
+      .addParam("Location", LocationTy)
+      .addParam("SampleIndex", IntTy)
+      .callBuiltin("__builtin_hlsl_resource_load_ms", ReturnType, PH::Handle,
+                   PH::_0, PH::_1)
+      .finalize();
+
+  // T Load(int2 location, int sampleIndex, int2 offset)
+  return BuiltinTypeMethodBuilder(*this, "Load", ReturnType)
+      .addParam("Location", LocationTy)
+      .addParam("SampleIndex", IntTy)
+      .addParam("Offset", OffsetTy)
+      .callBuiltin("__builtin_hlsl_resource_load_ms", ReturnType, PH::Handle,
+                   PH::_0, PH::_1, PH::_2)
+      .finalize();
+}
+
 BuiltinTypeDeclBuilder &
 BuiltinTypeDeclBuilder::addByteAddressBufferLoadMethods() {
   assert(!Record->isCompleteDefinition() && "record is already complete");
@@ -2237,6 +2310,26 @@ BuiltinTypeDeclBuilder::addSimpleTemplateParams(ArrayRef<StringRef> Names,
   return Builder.finalizeTemplateArgs(CD);
 }
 
+BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addMSTextureTemplateParams(
+    StringRef ElementName, StringRef SampleCountName, ConceptDecl *CD) {
+  if (Record->isCompleteDefinition()) {
+    assert(Template && "existing record it not a template");
+    assert(Template->getTemplateParameters()->size() == 2 &&
+           "template param count mismatch");
+    return *this;
+  }
+
+  ASTContext &AST = SemaRef.getASTContext();
+  TemplateParameterListBuilder Builder = TemplateParameterListBuilder(*this);
+  // No default element type (`Texture2DMS` and `Texture2DMS<>` are errors).
+  // A sample count of 0 means the count comes from the bound resource rather
+  // than denoting zero samples.
+  Builder.addTypeParameter(ElementName);
+  Builder.addNonTypeParameter(SampleCountName, AST.IntTy,
+                              getConstantIntExpr(0));
+  return Builder.finalizeTemplateArgs(CD);
+}
+
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addIncrementCounterMethod() {
   using PH = BuiltinTypeMethodBuilder::PlaceHolder;
   QualType UnsignedIntTy = SemaRef.getASTContext().UnsignedIntTy;
diff --git a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h
index 09cf1fceca116..fdcf8a92c396b 100644
--- a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h
+++ b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h
@@ -68,6 +68,12 @@ class BuiltinTypeDeclBuilder {
   BuiltinTypeDeclBuilder &
   addSimpleTemplateParams(ArrayRef<StringRef> Names,
                           ArrayRef<QualType> DefaultTypes, ConceptDecl *CD);
+  // Adds `<typename element_type, int sample_count = 0>` for multisampled
+  // textures, with \p CD constraining the element type. Unlike the other
+  // textures the element type has no default argument.
+  BuiltinTypeDeclBuilder &addMSTextureTemplateParams(StringRef ElementName,
+                                                     StringRef SampleCountName,
+                                                     ConceptDecl *CD);
   CXXRecordDecl *finalizeForwardDeclaration() { return Record; }
   BuiltinTypeDeclBuilder &completeDefinition();
 
@@ -81,7 +87,7 @@ class BuiltinTypeDeclBuilder {
                    AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &
   addTextureHandle(ResourceClass RC, bool IsROV, bool IsArray,
-                   ResourceDimension RD,
+                   ResourceDimension RD, bool IsMultiSampled = false,
                    AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &addSamplerHandle();
   BuiltinTypeDeclBuilder &addConstantBufferConversionToType();
@@ -104,6 +110,8 @@ class BuiltinTypeDeclBuilder {
   BuiltinTypeDeclBuilder &addLoadMethods();
   BuiltinTypeDeclBuilder &addTextureLoadMethods(ResourceDimension Dim,
                                                 bool IsArray = false);
+  BuiltinTypeDeclBuilder &addTextureLoadMSMethods(ResourceDimension Dim,
+                                                  bool IsArray = false);
   BuiltinTypeDeclBuilder &addByteAddressBufferLoadMethods();
   BuiltinTypeDeclBuilder &addByteAddressBufferStoreMethods();
   BuiltinTypeDeclBuilder &addSampleMethods(ResourceDimension Dim,
@@ -150,6 +158,7 @@ class BuiltinTypeDeclBuilder {
   addResourceMember(StringRef MemberName, ResourceClass RC,
                     ResourceDimension RD, bool IsROV, bool RawBuffer,
                     bool IsCounter, bool IsArray, QualType ElementTy,
+                    bool IsMultiSampled = false,
                     AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &addFriend(CXXRecordDecl *Friend);
   CXXRecordDecl *addPrivateNestedRecord(StringRef Name);
@@ -158,6 +167,7 @@ class BuiltinTypeDeclBuilder {
   BuiltinTypeDeclBuilder &
   addHandleMember(ResourceClass RC, ResourceDimension RD, bool IsROV,
                   bool RawBuffer, bool IsArray, QualType ElementTy,
+                  bool IsMultiSampled = false,
                   AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &
   addCounterHandleMember(ResourceClass RC, bool IsROV, bool RawBuffer,
diff --git a/clang/lib/Sema/HLSLExternalSemaSource.cpp b/clang/lib/Sema/HLSLExternalSemaSource.cpp
index 7578f20a27f18..8ce9b2976fe7d 100644
--- a/clang/lib/Sema/HLSLExternalSemaSource.cpp
+++ b/clang/lib/Sema/HLSLExternalSemaSource.cpp
@@ -297,6 +297,24 @@ static BuiltinTypeDeclBuilder setupRWTextureType(CXXRecordDecl *Decl, Sema &S,
       .addStaticInitializationFunctions(false);
 }
 
+/// Set up Texture2DMS (multisampled) type: SRV texture with only operator[]
+/// and sample-indexed Load. It does not support sampling, gather, LOD, or mips.
+static BuiltinTypeDeclBuilder setupMSTextureType(CXXRecordDecl *Decl, Sema &S,
+                                                 bool IsArray,
+                                                 ResourceDimension Dim) {
+  return BuiltinTypeDeclBuilder(S, Decl)
+      .addTextureHandle(ResourceClass::SRV, /*IsROV=*/false, IsArray, Dim,
+                        /*IsMultiSampled=*/true)
+      .addTextureLoadMSMethods(Dim, IsArray)
+      .addArraySubscriptOperators(Dim, IsArray)
+      // TODO: Add MS-specific GetDimensions (with a NumberOfSamples output);
+      // the generic addGetDimensionsMethods is mip-based and unsuitable here.
+      .addDefaultHandleConstructor()
+      .addCopyConstructor()
+      .addCopyAssignmentOperator()
+      .addStaticInitializationFunctions(false);
+}
+
 // Add a partial specialization for a template. The `TextureTemplate` is
 // `Texture<element_type>`, and it will be specialized for vectors:
 // `Texture<vector<element_type, element_count>>`.
@@ -765,6 +783,19 @@ void HLSLExternalSemaSource::defineHLSLTypesWithForwardDeclarations() {
                        ResourceDimension::Dim2D)
         .completeDefinition();
   });
+
+  // Texture2DMS — like Texture2D but multisampled, and the element type has no
+  // default argument.
+  Decl = BuiltinTypeDeclBuilder(*SemaPtr, HLSLNamespace, "Texture2DMS")
+             .addMSTextureTemplateParams("element_type", "sample_count",
+                                         TypedBufferConcept)
+             .finalizeForwardDeclaration();
+
+  onCompletion(Decl, [this](CXXRecordDecl *Decl) {
+    setupMSTextureType(Decl, *SemaPtr, /*IsArray=*/false,
+                       ResourceDimension::Dim2D)
+        .completeDefinition();
+  });
 }
 
 // Build a single overload of an HLSL atomic intrinsic in the hlsl namespace.
diff --git a/clang/lib/Sema/SemaHLSL.cpp b/clang/lib/Sema/SemaHLSL.cpp
index ae5e022399e49..30635ac0dd000 100644
--- a/clang/lib/Sema/SemaHLSL.cpp
+++ b/clang/lib/Sema/SemaHLSL.cpp
@@ -2096,7 +2096,8 @@ void SemaHLSL::handleShaderAttr(Decl *D, const ParsedAttr &AL) {
 
 bool clang::CreateHLSLAttributedResourceType(
     Sema &S, QualType Wrapped, ArrayRef<const Attr *> AttrList,
-    QualType &ResType, HLSLAttributedResourceLocInfo *LocInfo) {
+    QualType &ResType, HLSLAttributedResourceLocInfo *LocInfo,
+    Expr *SampleCountExpr) {
   assert(AttrList.size() && "expected list of resource attributes");
 
   QualType ContainedTy = QualType();
@@ -2167,6 +2168,7 @@ bool clang::CreateHLSLAttributedResourceType(
         return false;
       }
       ResAttrs.IsMultiSampled = true;
+      ResAttrs.SampleCountExpr = SampleCountExpr;
       break;
     case attr::HLSLIsCounter:
       if (ResAttrs.IsCounter) {
@@ -3950,6 +3952,49 @@ static bool CheckLoadLevelBuiltin(Sema &S, CallExpr *TheCall) {
   return false;
 }
 
+static bool CheckLoadMSBuiltin(Sema &S, CallExpr *TheCall) {
+  if (S.checkArgCountRange(TheCall, 3, 4))
+    return true;
+
+  // Check the multisampled texture handle.
+  if (CheckResourceHandle(&S, TheCall, 0,
+                          [](const HLSLAttributedResourceType *ResType) {
+                            return !ResType->getAttrs().IsMultiSampled;
+                          }))
+    return true;
+
+  auto *ResourceTy =
+      TheCall->getArg(0)->getType()->castAs<HLSLAttributedResourceType>();
+
+  // Check the location (int2 for Texture2DMS, int3 for Texture2DMSArray).
+  // Unlike Load on regular textures, there is no mip/LOD component.
+  unsigned ResourceDim =
+      getResourceDimensions(ResourceTy->getAttrs().ResourceDimension);
+  unsigned LocationDim = ResourceDim + (ResourceTy->getAttrs().IsArray ? 1 : 0);
+  QualType LocationTy = TheCall->getArg(1)->getType();
+  if (CheckVectorElementCount(&S, LocationTy, S.Context.IntTy, LocationDim,
+                              TheCall->getArg(1)->getBeginLoc()))
+    return true;
+
+  // Check the sample index operand (scalar int).
+  if (!TheCall->getArg(2)->getType()->isIntegerType()) {
+    S.Diag(TheCall->getArg(2)->getBeginLoc(), diag::err_typecheck_expect_int)
+        << TheCall->getArg(2)->getType();
+    return true;
+  }
+
+  // Check the offset operand (int2 for 2D textures; no array slice).
+  if (TheCall->getNumArgs() > 3) {
+    if (CheckVectorElementCount(&S, TheCall->getArg(3)->getType(),
+                                S.Context.IntTy, ResourceDim,
+                                TheCall->getArg(3)->getBeginLoc()))
+      return true;
+  }
+
+  TheCall->setType(ResourceTy->getContainedType());
+  return false;
+}
+
 static bool CheckSamplingBuiltin(Sema &S, CallExpr *TheCall, SampleKind Kind) {
   unsigned MinArgs, MaxArgs;
   if (Kind == SampleKind::Sample) {
@@ -4173,6 +4218,8 @@ bool SemaHLSL::CheckBuiltinFunctionCall(unsigned BuiltinID, CallExpr *TheCall) {
   }
   case Builtin::BI__builtin_hlsl_resource_load_level:
     return CheckLoadLevelBuiltin(SemaRef, TheCall);
+  case Builtin::BI__builtin_hlsl_resource_load_ms:
+    return CheckLoadMSBuiltin(SemaRef, TheCall);
   case Builtin::BI__builtin_hlsl_resource_sample:
     return CheckSamplingBuiltin(SemaRef, TheCall, SampleKind::Sample);
   case Builtin::BI__builtin_hlsl_resource_sample_bias:
diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h
index 8ed1dba3bf101..b5f4f1247025e 100644
--- a/clang/lib/Sema/TreeTransform.h
+++ b/clang/lib/Sema/TreeTransform.h
@@ -7824,11 +7824,21 @@ QualType TreeTransform<Derived>::TransformHLSLAttributedResourceType(
     ContainedTy = ContainedTSI->getType();
   }
 
+  HLSLAttributedResourceType::Attributes Attrs = oldType->getAttrs();
+  if (Attrs.SampleCountExpr) {
+    ExprResult SampleCountResult =
+        getDerived().TransformExpr(Attrs.SampleCountExpr);
+    if (SampleCountResult.isInvalid())
+      return QualType();
+    Attrs.SampleCountExpr = SampleCountResult.get();
+  }
+
   QualType Result = TL.getType();
   if (getDerived().AlwaysRebuild() || WrappedTy != oldType->getWrappedType() ||
-      ContainedTy != oldType->getContainedType()) {
-    Result = SemaRef.Context.getHLSLAttributedResourceType(
-        WrappedTy, ContainedTy, oldType->getAttrs());
+      ContainedTy != oldType->getContainedType() ||
+      Attrs.SampleCountExpr != oldType->getSampleCountExpr()) {
+    Result = SemaRef.Context.getHLSLAttributedResourceType(WrappedTy,
+                                                           ContainedTy, Attrs);
   }
 
   HLSLAttributedResourceTypeLoc NewTL =
diff --git a/clang/test/AST/HLSL/MultiSampledTextures-AST.hlsl b/clang/test/AST/HLSL/MultiSampledTextures-AST.hlsl
new file mode 100644
index 0000000000000..bd2a3b17c61c7
--- /dev/null
+++ b/clang/test/AST/HLSL/MultiSampledTextures-AST.hlsl
@@ -0,0 +1,97 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -ast-dump -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -o - %s | FileCheck %s -DTEXTURE=Texture2DMS -DINDEX_SIZE=2
+
+// The class template: an `element_type` type parameter followed by an `int`
+// `sample_count` non-type parameter that defaults to 0.
+// CHECK: ClassTemplateDecl {{.*}} [[TEXTURE]]
+// CHECK: TemplateTypeParmDecl {{.*}} element_type
+// CHECK: NonTypeTemplateParmDecl {{.*}} 'int' depth 0 index 1 sample_count
+// CHECK: TemplateArgument expr '0'
+
+// The resource handle: an SRV whose handle additionally carries [[hlsl::is_ms]].
+// CHECK: CXXRecordDecl {{.*}} [[TEXTURE]] definition
+// CHECK: FinalAttr {{.*}} Implicit final
+// CHECK-NEXT: FieldDecl {{.*}} implicit __handle '__hlsl_resource_t
+// CHECK-SAME{LITERAL}: [[hlsl::resource_class(SRV)]]
+// CHECK-SAME{LITERAL}: [[hlsl::is_ms]]
+// CHECK-SAME{LITERAL}: [[hlsl::contained_type(element_type)]]
+// CHECK-SAME{LITERAL}: [[hlsl::resource_dimension(2D)]]
+
+// Load(location, sampleIndex): the sample index is a separate scalar (there is
+// no packed mip level), and the read lowers to __builtin_hlsl_resource_load_ms.
+// CHECK: CXXMethodDecl {{.*}} Load 'element_type (vector<int, [[INDEX_SIZE]]>, int)' inline
+// CHECK-NEXT: ParmVarDecl {{.*}} Location 'vector<int, [[INDEX_SIZE]]>'
+// CHECK-NEXT: ParmVarDecl {{.*}} SampleIndex 'int'
+// CHECK-NEXT: CompoundStmt
+// CHECK-NEXT: ReturnStmt
+// CHECK-NEXT: CStyleCastExpr {{.*}} 'element_type' <Dependent>
+// CHECK-NEXT: CallExpr {{.*}} '<dependent type>'
+// CHECK-NEXT: DeclRefExpr {{.*}} '<builtin fn type>' Function {{.*}} '__builtin_hlsl_resource_load_ms' 'void (...) noexcept'
+// CHECK-NEXT: MemberExpr {{.*}} '__hlsl_resource_t
+// CHECK-SAME{LITERAL}: [[hlsl::resource_class(SRV)]]
+// CHECK-SAME{LITERAL}: [[hlsl::is_ms]]
+// CHECK-SAME{LITERAL}: [[hlsl::contained_type(element_type)]]
+// CHECK-SAME{LITERAL}: [[hlsl::resource_dimension(2D)]]
+// CHECK-SAME: ' lvalue .__handle
+// CHECK-NEXT: CXXThisExpr {{.*}} 'hlsl::[[TEXTURE]]<element_type, sample_count>' lvalue implicit this
+// CHECK-NEXT: DeclRefExpr {{.*}} 'vector<int, [[INDEX_SIZE]]>' lvalue ParmVar {{.*}} 'Location' 'vector<int, [[INDEX_SIZE]]>'
+// CHECK-NEXT: DeclRefExpr {{.*}} 'int' lvalue ParmVar {{.*}} 'SampleIndex' 'int'
+// CHECK-NEXT: AlwaysInlineAttr
+
+// Load(location, sampleIndex, offset): identical, with a trailing 2D offset.
+// CHECK: CXXMethodDecl {{.*}} Load 'element_type (vector<int, [[INDEX_SIZE]]>, int, vector<int, 2>)' inline
+// CHECK-NEXT: ParmVarDecl {{.*}} Location 'vector<int, [[INDEX_SIZE]]>'
+// CHECK-NEXT: ParmVarDecl {{.*}} SampleIndex 'int'
+// CHECK-NEXT: ParmVarDecl {{.*}} Offset 'vector<int, 2>'
+// CHECK-NEXT: CompoundStmt
+// CHECK-NEXT: ReturnStmt
+// CHECK-NEXT: CStyleCastExpr {{.*}} 'element_type' <Dependent>
+// CHECK-NEXT: CallExpr {{.*}} '<dependent type>'
+// CHECK-NEXT: DeclRefExpr {{.*}} '<builtin fn type>' Function {{.*}} '__builtin_hlsl_resource_load_ms' 'void (...) noexcept'
+// CHECK-NEXT: MemberExpr {{.*}} '__hlsl_resource_t
+// CHECK-SAME: ' lvalue .__handle
+// CHECK-NEXT: CXXThisExpr {{.*}} 'hlsl::[[TEXTURE]]<element_type, sample_count>' lvalue implicit this
+// CHECK-NEXT: DeclRefExpr {{.*}} 'vector<int, [[INDEX_SIZE]]>' lvalue ParmVar {{.*}} 'Location' 'vector<int, [[INDEX_SIZE]]>'
+// CHECK-NEXT: DeclRefExpr {{.*}} 'int' lvalue ParmVar {{.*}} 'SampleIndex' 'int'
+// CHECK-NEXT: DeclRefExpr {{.*}} 'vector<int, 2>' lvalue ParmVar {{.*}} 'Offset' 'vector<int, 2>'
+// CHECK-NEXT: AlwaysInlineAttr
+
+// operator[] returns a const reference to sample 0 (same shape as the non-MS
+// SRV subscript in Textures-AST.hlsl), via __builtin_hlsl_resource_getpointer.
+// CHECK: CXXMethodDecl {{.*}} operator[] 'const hlsl_device element_type &(vector<unsigned int, [[INDEX_SIZE]]>) const' inline
+// CHECK-NEXT: ParmVarDecl {{.*}} Index 'vector<unsigned int, [[INDEX_SIZE]]>'
+// CHECK-NEXT: CompoundStmt
+// CHECK-NEXT: ReturnStmt
+// CHECK-NEXT: UnaryOperator {{.*}} 'hlsl_device element_type' lvalue prefix '*' cannot overflow
+// CHECK-NEXT: CStyleCastExpr {{.*}} 'hlsl_device element_type *' <Dependent>
+// CHECK-NEXT: CallExpr {{.*}} '<dependent type>'
+// CHECK-NEXT: DeclRefExpr {{.*}} '<builtin fn type>' Function {{.*}} '__builtin_hlsl_resource_getpointer' 'void (...) noexcept'
+// CHECK-NEXT: MemberExpr {{.*}} '__hlsl_resource_t
+// CHECK-SAME: ' lvalue .__handle
+// CHECK-NEXT: CXXThisExpr {{.*}} 'const hlsl::[[TEXTURE]]<element_type, sample_count>' lvalue implicit this
+// CHECK-NEXT: DeclRefExpr {{.*}} 'vector<unsigned int, [[INDEX_SIZE]]>' lvalue ParmVar {{.*}} 'Index' 'vector<unsigned int, [[INDEX_SIZE]]>'
+// CHECK-NEXT: AlwaysInlineAttr
+
+// TODO(MS GetDimensions): when the multisampled GetDimensions overload is
+// implemented, dump its CHECK block here (out width/height[/elements]/
+// NumberOfSamples, on a samples-based getdimensions builtin), paralleling the
+// GetDimensions blocks in Textures-AST.hlsl.
+
+TEXTURE<float> t;
+
+// An explicit, non-default sample count binds the non-type template parameter,
+// producing a distinct specialization.
+// CHECK: ClassTemplateSpecializationDecl {{.*}} class [[TEXTURE]] definition
+// CHECK: TemplateArgument type 'vector<float, 4>'
+// CHECK: TemplateArgument integral '4'
+TEXTURE<float4, 4> tMS4;
+
+[numthreads(1, 1, 1)]
+void main() {
+  uint2 i = uint2(0, 0);
+  float x = t[i];
+  (void)x;
+  // TODO: enable once multisampled GetDimensions is implemented, paralleling
+  // the t.GetDimensions(w, h) call in Textures-AST.hlsl:
+  // uint w, h, n;
+  // t.GetDimensions(w, h, n);
+}
diff --git a/clang/test/CodeGenHLSL/resources/MultiSampledTextures-Load.hlsl b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-Load.hlsl
new file mode 100644
index 0000000000000..f8cc6e2b72b62
--- /dev/null
+++ b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-Load.hlsl
@@ -0,0 +1,64 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -o - %s | FileCheck %s --check-prefix=DXIL -DCOORD_DIM=2 -DKIND=3
+// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -o - %s | FileCheck %s --check-prefix=SPIRV -DCOORD_DIM=2 -DARRAYED=0
+
+// Load on a multisampled texture takes a separate sample index (rather than a
+// packed mip level) and lowers to the MS-specific resource.load.ms intrinsic on
+// a dx.MSTexture (DXIL) / multisampled spirv.Image (SPIR-V) handle. The correct
+// per-element-type intrinsic overload is selected, and signed integer element
+// types map to a SignedImage / IsSigned=1 handle. The resource type layout
+// itself is covered by MultiSampledTextures-default.hlsl.
+
+TEXTURE<float4> T;
+TEXTURE<float4, 4> TMS4;
+TEXTURE<float> Tf;
+TEXTURE<int> Ti;
+TEXTURE<int4> Ti4;
+
+// CHECK-LABEL: define {{.*}} <4 x float> @{{.*}}test_load
+// DXIL: call {{.*}} <4 x float> @llvm.dx.resource.load.ms.v4f32.{{.*}}(target("dx.MSTexture", <4 x float>, 0, 0, 0, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+// SPIRV: call {{.*}} <4 x float> @llvm.spv.resource.load.ms.v4f32.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+float4 test_load(LOCATION_TYPE loc, int sampleIndex) {
+  return T.Load(loc, sampleIndex);
+}
+
+// The offset overload threads the constant offset through as the final operand.
+// CHECK-LABEL: define {{.*}} <4 x float> @{{.*}}test_load_offset
+// DXIL: call {{.*}} <4 x float> @llvm.dx.resource.load.ms.v4f32.{{.*}}(target("dx.MSTexture", <4 x float>, 0, 0, 0, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> %{{.*}})
+// SPIRV: call {{.*}} <4 x float> @llvm.spv.resource.load.ms.v4f32.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> %{{.*}})
+float4 test_load_offset(LOCATION_TYPE loc, int sampleIndex) {
+  return T.Load(loc, sampleIndex, int2(1, 1));
+}
+
+// An explicit compile-time sample count changes only the resource handle type
+// (the dx.MSTexture sample-count operand becomes 4); the resource.load.ms call
+// is otherwise identical to the default.
+// CHECK-LABEL: define {{.*}} <4 x float> @{{.*}}test_explicit_count
+// DXIL: call {{.*}} <4 x float> @llvm.dx.resource.load.ms.v4f32.{{.*}}(target("dx.MSTexture", <4 x float>, 0, 4, 0, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+// SPIRV: call {{.*}} <4 x float> @llvm.spv.resource.load.ms.v4f32.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+float4 test_explicit_count(LOCATION_TYPE loc, int sampleIndex) {
+  return TMS4.Load(loc, sampleIndex);
+}
+
+// A scalar float element selects the f32 overload.
+// CHECK-LABEL: define {{.*}} float @{{.*}}test_load_float
+// DXIL: call {{.*}} float @llvm.dx.resource.load.ms.f32.{{.*}}(target("dx.MSTexture", float, 0, 0, 0, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+// SPIRV: call {{.*}} float @llvm.spv.resource.load.ms.f32.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+float test_load_float(LOCATION_TYPE loc, int sampleIndex) {
+  return Tf.Load(loc, sampleIndex);
+}
+
+// A signed integer element maps to IsSigned=1 (DXIL) / spirv.SignedImage.
+// CHECK-LABEL: define {{.*}} i32 @{{.*}}test_load_int
+// DXIL: call i32 @llvm.dx.resource.load.ms.i32.{{.*}}(target("dx.MSTexture", i32, 0, 0, 1, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+// SPIRV: call i32 @llvm.spv.resource.load.ms.i32.{{.*}}(target("spirv.SignedImage", i32, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+int test_load_int(LOCATION_TYPE loc, int sampleIndex) {
+  return Ti.Load(loc, sampleIndex);
+}
+
+// A signed integer vector element: v4i32 overload on a SignedImage handle.
+// CHECK-LABEL: define {{.*}} <4 x i32> @{{.*}}test_load_int4
+// DXIL: call <4 x i32> @llvm.dx.resource.load.ms.v4i32.{{.*}}(target("dx.MSTexture", <4 x i32>, 0, 0, 1, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+// SPIRV: call <4 x i32> @llvm.spv.resource.load.ms.v4i32.{{.*}}(target("spirv.SignedImage", i32, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+int4 test_load_int4(LOCATION_TYPE loc, int sampleIndex) {
+  return Ti4.Load(loc, sampleIndex);
+}
diff --git a/clang/test/CodeGenHLSL/resources/MultiSampledTextures-default-explicit-binding.hlsl b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-default-explicit-binding.hlsl
new file mode 100644
index 0000000000000..045174314a383
--- /dev/null
+++ b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-default-explicit-binding.hlsl
@@ -0,0 +1,31 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -o - %s | llvm-cxxfilt | FileCheck %s --check-prefixes=CHECK,DXIL -DTEXTURE=Texture2DMS -DCOORD_DIM=2 -DKIND=3
+// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -o - %s | llvm-cxxfilt | FileCheck %s --check-prefixes=CHECK,SPIRV -DTEXTURE=Texture2DMS -DCOORD_DIM=2 -DARRAYED=0
+
+// A multisampled texture with an explicit register/space binding is initialized
+// from that binding through __createFromBinding, identically to a non-MS
+// texture; only the resource handle type differs (dx.MSTexture / a multisampled
+// spirv.Image). Unlike non-multisampled textures, the element type must be
+// stated explicitly (Texture2DMS and Texture2DMS<> are both errors), so the
+// default-template / shorthand forms are not exercised here.
+
+TEXTURE<float4> explicit_binding : register(t1, space2);
+TEXTURE<float4> implicit_template : register(t0, space1);
+
+// DXIL: %"class.hlsl::[[TEXTURE]]" = type { target("dx.MSTexture", <4 x float>, 0, 0, 0, [[KIND]]) }
+// SPIRV: %"class.hlsl::[[TEXTURE]]" = type { target("spirv.Image", float, 1, 2, [[ARRAYED]], 1, 1, 0) }
+
+// CHECK: @{{.*}}explicit_binding = internal global %"class.hlsl::[[TEXTURE]]" poison, align {{[0-9]+}}
+// CHECK: @{{.*}}implicit_template = internal global %"class.hlsl::[[TEXTURE]]" poison, align {{[0-9]+}}
+
+// Each texture is initialized from its explicit register/space binding:
+//   explicit_binding  -> register(t1, space2)  =>  registerNo 1, spaceNo 2
+//   implicit_template -> register(t0, space1)  =>  registerNo 0, spaceNo 1
+// CHECK: call {{.*}} @hlsl::[[TEXTURE]]<float vector[4]{{(, [0-9]+)?}}>::__createFromBinding{{.*}}(ptr {{.*}}@{{.*}}explicit_binding, i32 noundef 1, i32 noundef 2, i32 noundef 1, i32 noundef 0, ptr noundef @{{.*}})
+// CHECK: call {{.*}} @hlsl::[[TEXTURE]]<float vector[4]{{(, [0-9]+)?}}>::__createFromBinding{{.*}}(ptr {{.*}}@{{.*}}implicit_template, i32 noundef 0, i32 noundef 1, i32 noundef 1, i32 noundef 0, ptr noundef @{{.*}})
+
+float4 main(LOCATION_TYPE loc : LOC, int sampleIndex : SI) : SV_Target {
+  // A multisampled texture is read through a sample-indexed Load, not Sample.
+  // DXIL: call {{.*}} <4 x float> @llvm.dx.resource.load.ms.v4f32.{{.*}}(target("dx.MSTexture", <4 x float>, 0, 0, 0, [[KIND]]) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+  // SPIRV: call {{.*}} <4 x float> @llvm.spv.resource.load.ms.v4f32.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 1, 1, 0) %{{.*}}, <[[COORD_DIM]] x i32> %{{.*}}, i32 %{{.*}}, <2 x i32> zeroinitializer)
+  return implicit_template.Load(loc, sampleIndex);
+}
diff --git a/clang/test/CodeGenHLSL/resources/MultiSampledTextures-default.hlsl b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-default.hlsl
new file mode 100644
index 0000000000000..e4980f3d2fb83
--- /dev/null
+++ b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-default.hlsl
@@ -0,0 +1,27 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -o - %s | FileCheck %s --check-prefix=DXIL -DTEXTURE=Texture2DMS -DDXIL_ARGS="<4 x float>, 0, 0, 0, 3" -DDXIL_ARGS4="<4 x float>, 0, 4, 0, 3"
+// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -DTEXTURE=Texture2DMS -o - %s | FileCheck %s --check-prefix=SPIRV -DTEXTURE=Texture2DMS -DSPIRV_ARGS="float, 1, 2, 0, 1, 1, 0"
+
+// A multisampled texture lowers to a multisampled resource target type. For
+// DXIL this is the dedicated `dx.MSTexture` type carrying the multisampled
+// resource kind; for SPIR-V it is a `spirv.Image` with the Multisampled (MS)
+// operand set to 1.
+//
+// The Texture2DMS<T, N> sample count N is the second integer operand of
+// dx.MSTexture: it is 0 for the default (T, runtime-determined) and 4 for the
+// explicit-count global (TMS4), producing two distinct resource types. SPIR-V
+// does not encode the count, so both map to the same spirv.Image.
+
+// DXIL: %"class.hlsl::[[TEXTURE]]" = type { target("dx.MSTexture", [[DXIL_ARGS]]) }
+// DXIL: %"class.hlsl::[[TEXTURE]].0" = type { target("dx.MSTexture", [[DXIL_ARGS4]]) }
+// SPIRV: %"class.hlsl::[[TEXTURE]]" = type { target("spirv.Image", [[SPIRV_ARGS]]) }
+// SPIRV: %"class.hlsl::[[TEXTURE]].0" = type { target("spirv.Image", [[SPIRV_ARGS]]) }
+
+// DXIL: @{{.*}}T = internal global %"class.hlsl::[[TEXTURE]]" poison
+// SPIRV: @{{.*}}T = internal global %"class.hlsl::[[TEXTURE]]" poison
+TEXTURE<float4> T;
+
+// DXIL: @{{.*}}TMS4 = internal global %"class.hlsl::[[TEXTURE]].0" poison
+// SPIRV: @{{.*}}TMS4 = internal global %"class.hlsl::[[TEXTURE]].0" poison
+TEXTURE<float4, 4> TMS4;
+
+void main() {}
diff --git a/clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl
new file mode 100644
index 0000000000000..7e3b5b981648c
--- /dev/null
+++ b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl
@@ -0,0 +1,23 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -emit-pch -o %t %s
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -include-pch %t -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s -DTEXTURE=Texture2DMS -DKIND=3
+
+// The Texture2DMS<T, N> sample count is part of the resource handle type, so it
+// has to survive serialization: a deserialized handle still lowers to a
+// dx.MSTexture with sample count 4.
+
+#ifndef HEADER
+#define HEADER
+
+TEXTURE<float4, 4> TMS4;
+
+#else
+
+// CHECK: %"class.hlsl::[[TEXTURE]]" = type { target("dx.MSTexture", <4 x float>, 0, 4, 0, [[KIND]]) }
+
+[numthreads(1, 1, 1)]
+void main() {
+  float4 V = TMS4.Load((LOCATION_TYPE)0, 0);
+  (void)V;
+}
+
+#endif
diff --git a/clang/test/CodeGenHLSL/resources/Textures-Subscript.hlsl b/clang/test/CodeGenHLSL/resources/Textures-Subscript.hlsl
index 1d765ee9cd0a1..686df4195f68e 100644
--- a/clang/test/CodeGenHLSL/resources/Textures-Subscript.hlsl
+++ b/clang/test/CodeGenHLSL/resources/Textures-Subscript.hlsl
@@ -1,7 +1,18 @@
-// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2D -DCOORD_TYPE=uint2 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2D -DCOORD_DIM=2 --check-prefixes=CHECK,DXIL -DDXIL_TY=2 -DRW=0
-// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2D -DCOORD_TYPE=uint2 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2D -DCOORD_DIM=2 --check-prefixes=CHECK,SPIRV -DARRAYED=0 -DSAMPLED=1 -DIMG_FMT=0
-// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2DArray -DCOORD_TYPE=uint3 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2DArray -DCOORD_DIM=3 --check-prefixes=CHECK,DXIL -DDXIL_TY=7 -DRW=0
-// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2DArray -DCOORD_TYPE=uint3 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2DArray -DCOORD_DIM=3 --check-prefixes=CHECK,SPIRV -DARRAYED=1 -DSAMPLED=1 -DIMG_FMT=0
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2D -DCOORD_TYPE=uint2 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2D -DCOORD_DIM=2 --check-prefixes=CHECK,DXIL -DROV_OR_COUNT=0 -DDXIL_HANDLE=dx.Texture -DDXIL_TY=2 -DRW=0
+// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2D -DCOORD_TYPE=uint2 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2D -DCOORD_DIM=2 --check-prefixes=CHECK,SPIRV -DARRAYED=0 -DMS=0 -DSAMPLED=1 -DIMG_FMT=0
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2DArray -DCOORD_TYPE=uint3 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2DArray -DCOORD_DIM=3 --check-prefixes=CHECK,DXIL -DROV_OR_COUNT=0 -DDXIL_HANDLE=dx.Texture -DDXIL_TY=7 -DRW=0
+// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2DArray -DCOORD_TYPE=uint3 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2DArray -DCOORD_DIM=3 --check-prefixes=CHECK,SPIRV -DARRAYED=1 -DMS=0 -DSAMPLED=1 -DIMG_FMT=0
+
+// Texture2DMS reuses the same operator[] codegen; only the resource handle type
+// differs (dx.MSTexture / a multisampled spirv.Image). It reads sample 0.
+//
+// ROV_OR_COUNT is the overloaded second int operand of the DXIL handle type:
+// IsROV for dx.Texture, the sample count for dx.MSTexture. It is 0 for every
+// texture below (none are ROVs, and Texture2DMS<T> defaults to a runtime
+// sample count), but a ROV or an explicit Texture2DMS<T, N> would need its own
+// value here.
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2DMS -DCOORD_TYPE=uint2 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2DMS -DCOORD_DIM=2 --check-prefixes=CHECK,DXIL -DROV_OR_COUNT=0 -DDXIL_HANDLE=dx.MSTexture -DDXIL_TY=3 -DRW=0
+// RUN: %clang_cc1 -triple spirv-vulkan-library -x hlsl -emit-llvm -disable-llvm-passes -finclude-default-header -Wno-sign-conversion -DTEXTURE=Texture2DMS -DCOORD_TYPE=uint2 -o - %s | llvm-cxxfilt | FileCheck %s -DTEXTURE=Texture2DMS -DCOORD_DIM=2 --check-prefixes=CHECK,SPIRV -DARRAYED=0 -DMS=1 -DSAMPLED=1 -DIMG_FMT=0
 
 TEXTURE<float4> Tex : register(t0);
 TEXTURE<float> Tex2 : register(t1);
@@ -21,56 +32,56 @@ void main(COORD_TYPE DTid : SV_DispatchThreadID) {
 // CHECK: %[[VAL3:.*]] = alloca <3 x i32>
 // CHECK: store <[[COORD_DIM]] x i32> %[[DTID]], ptr %[[DTID_ADDR]]
 // CHECK: %[[DTID_VAL:.*]] = load <[[COORD_DIM]] x i32>, ptr %[[DTID_ADDR]]
-// CHECK: %[[CALL1:.*]] = call noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float vector[4]>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) @Tex, <[[COORD_DIM]] x i32> noundef %[[DTID_VAL]])
+// CHECK: %[[CALL1:.*]] = call noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float vector[4]{{(, [0-9]+)?}}>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) @Tex, <[[COORD_DIM]] x i32> noundef %[[DTID_VAL]])
 // CHECK: %[[LOAD_VAL:.*]] = load <4 x float>, ptr{{.*}} %[[CALL1]]
 // CHECK: store <4 x float> %[[LOAD_VAL]], ptr %[[VAL]]
 // CHECK: %[[DTID_VAL2:.*]] = load <[[COORD_DIM]] x i32>, ptr %[[DTID_ADDR]]
-// CHECK: %[[CALL2:.*]] = call noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) @Tex2, <[[COORD_DIM]] x i32> noundef %[[DTID_VAL2]])
+// CHECK: %[[CALL2:.*]] = call noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float{{(, [0-9]+)?}}>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) @Tex2, <[[COORD_DIM]] x i32> noundef %[[DTID_VAL2]])
 // CHECK: %[[LOAD_VAL2:.*]] = load float, ptr{{.*}} %[[CALL2]]
 // CHECK: store float %[[LOAD_VAL2]], ptr %[[VAL2]]
 // CHECK: %[[DTID_VAL3:.*]] = load <[[COORD_DIM]] x i32>, ptr %[[DTID_ADDR]]
-// CHECK: %[[CALL3:.*]] = call noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<int vector[3]>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) @Tex3, <[[COORD_DIM]] x i32> noundef %[[DTID_VAL3]])
+// CHECK: %[[CALL3:.*]] = call noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<int vector[3]{{(, [0-9]+)?}}>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) @Tex3, <[[COORD_DIM]] x i32> noundef %[[DTID_VAL3]])
 // CHECK: %[[LOAD_VAL3:.*]] = load <3 x i32>, ptr{{.*}} %[[CALL3]]
 // CHECK: store <3 x i32> %[[LOAD_VAL3]], ptr %[[VAL3]]
 
-// CHECK: define linkonce_odr hidden noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float vector[4]>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) %[[THIS:.*]], <[[COORD_DIM]] x i32> noundef %[[INDEX:.*]])
+// CHECK: define linkonce_odr hidden noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float vector[4]{{(, [0-9]+)?}}>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) %[[THIS:.*]], <[[COORD_DIM]] x i32> noundef %[[INDEX:.*]])
 // CHECK: %[[THIS_ADDR:.*]] = alloca ptr
 // CHECK: %[[INDEX_ADDR:.*]] = alloca <[[COORD_DIM]] x i32>
 // CHECK: store ptr %[[THIS]], ptr %[[THIS_ADDR]]
 // CHECK: store <[[COORD_DIM]] x i32> %[[INDEX]], ptr %[[INDEX_ADDR]]
 // CHECK: %[[THIS1:.*]] = load ptr, ptr %[[THIS_ADDR]]
 // CHECK: %[[HANDLE_PTR:.*]] = getelementptr {{.*}} %"class.hlsl::[[TEXTURE]]", ptr %[[THIS1]], i32 0, i32 0
-// DXIL: %[[HANDLE:.*]] = load target("dx.Texture", <4 x float>, [[RW]], 0, 0, [[DXIL_TY]]), ptr %[[HANDLE_PTR]]
-// SPIRV: %[[HANDLE:.*]] = load target("spirv.Image", float, 1, 2, [[ARRAYED]], 0, [[SAMPLED]], [[IMG_FMT]]), ptr %[[HANDLE_PTR]]
+// DXIL: %[[HANDLE:.*]] = load target("[[DXIL_HANDLE]]", <4 x float>, [[RW]], [[ROV_OR_COUNT]], 0, [[DXIL_TY]]), ptr %[[HANDLE_PTR]]
+// SPIRV: %[[HANDLE:.*]] = load target("spirv.Image", float, 1, 2, [[ARRAYED]], [[MS]], [[SAMPLED]], [[IMG_FMT]]), ptr %[[HANDLE_PTR]]
 // CHECK: %[[INDEX_VAL:.*]] = load <[[COORD_DIM]] x i32>, ptr %[[INDEX_ADDR]]
-// DXIL: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.{{.*}}(target("dx.Texture", <4 x float>, [[RW]], 0, 0, [[DXIL_TY]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
-// SPIRV: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 0, [[SAMPLED]], [[IMG_FMT]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
+// DXIL: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.{{.*}}(target("[[DXIL_HANDLE]]", <4 x float>, [[RW]], [[ROV_OR_COUNT]], 0, [[DXIL_TY]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
+// SPIRV: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], [[MS]], [[SAMPLED]], [[IMG_FMT]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
 // CHECK: ret ptr {{.*}}%[[PTR]]
 
-// CHECK: define linkonce_odr hidden noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) %[[THIS:.*]], <[[COORD_DIM]] x i32> noundef %[[INDEX:.*]])
+// CHECK: define linkonce_odr hidden noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<float{{(, [0-9]+)?}}>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) %[[THIS:.*]], <[[COORD_DIM]] x i32> noundef %[[INDEX:.*]])
 // CHECK: %[[THIS_ADDR:.*]] = alloca ptr
 // CHECK: %[[INDEX_ADDR:.*]] = alloca <[[COORD_DIM]] x i32>
 // CHECK: store ptr %[[THIS]], ptr %[[THIS_ADDR]]
 // CHECK: store <[[COORD_DIM]] x i32> %[[INDEX]], ptr %[[INDEX_ADDR]]
 // CHECK: %[[THIS1:.*]] = load ptr, ptr %[[THIS_ADDR]]
 // CHECK: %[[HANDLE_PTR:.*]] = getelementptr {{.*}} %"class.hlsl::[[TEXTURE]].0", ptr %[[THIS1]], i32 0, i32 0
-// DXIL: %[[HANDLE:.*]] = load target("dx.Texture", float, [[RW]], 0, 0, [[DXIL_TY]]), ptr %[[HANDLE_PTR]]
-// SPIRV: %[[HANDLE:.*]] = load target("spirv.Image", float, 1, 2, [[ARRAYED]], 0, [[SAMPLED]], [[IMG_FMT]]), ptr %[[HANDLE_PTR]]
+// DXIL: %[[HANDLE:.*]] = load target("[[DXIL_HANDLE]]", float, [[RW]], [[ROV_OR_COUNT]], 0, [[DXIL_TY]]), ptr %[[HANDLE_PTR]]
+// SPIRV: %[[HANDLE:.*]] = load target("spirv.Image", float, 1, 2, [[ARRAYED]], [[MS]], [[SAMPLED]], [[IMG_FMT]]), ptr %[[HANDLE_PTR]]
 // CHECK: %[[INDEX_VAL:.*]] = load <[[COORD_DIM]] x i32>, ptr %[[INDEX_ADDR]]
-// DXIL: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.{{.*}}(target("dx.Texture", float, [[RW]], 0, 0, [[DXIL_TY]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
-// SPIRV: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], 0, [[SAMPLED]], [[IMG_FMT]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
+// DXIL: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.{{.*}}(target("[[DXIL_HANDLE]]", float, [[RW]], [[ROV_OR_COUNT]], 0, [[DXIL_TY]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
+// SPIRV: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.{{.*}}(target("spirv.Image", float, 1, 2, [[ARRAYED]], [[MS]], [[SAMPLED]], [[IMG_FMT]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
 // CHECK: ret ptr {{.*}}%[[PTR]]
 
-// CHECK: define linkonce_odr hidden noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<int vector[3]>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) %[[THIS:.*]], <[[COORD_DIM]] x i32> noundef %[[INDEX:.*]])
+// CHECK: define linkonce_odr hidden noundef {{.*}}ptr{{.*}} @hlsl::[[TEXTURE]]<int vector[3]{{(, [0-9]+)?}}>::operator[](unsigned int vector[[[COORD_DIM]]]) const(ptr noundef nonnull align {{[0-9]+}} dereferenceable({{[0-9]+}}) %[[THIS:.*]], <[[COORD_DIM]] x i32> noundef %[[INDEX:.*]])
 // CHECK: %[[THIS_ADDR:.*]] = alloca ptr
 // CHECK: %[[INDEX_ADDR:.*]] = alloca <[[COORD_DIM]] x i32>
 // CHECK: store ptr %[[THIS]], ptr %[[THIS_ADDR]]
 // CHECK: store <[[COORD_DIM]] x i32> %[[INDEX]], ptr %[[INDEX_ADDR]]
 // CHECK: %[[THIS1:.*]] = load ptr, ptr %[[THIS_ADDR]]
 // CHECK: %[[HANDLE_PTR:.*]] = getelementptr {{.*}} %"class.hlsl::[[TEXTURE]].1", ptr %[[THIS1]], i32 0, i32 0
-// DXIL: %[[HANDLE:.*]] = load target("dx.Texture", <3 x i32>, [[RW]], 0, 1, [[DXIL_TY]]), ptr %[[HANDLE_PTR]]
-// SPIRV: %[[HANDLE:.*]] = load target("spirv.SignedImage", i32, 1, 2, [[ARRAYED]], 0, [[SAMPLED]], [[IMG_FMT]]), ptr %[[HANDLE_PTR]]
+// DXIL: %[[HANDLE:.*]] = load target("[[DXIL_HANDLE]]", <3 x i32>, [[RW]], [[ROV_OR_COUNT]], 1, [[DXIL_TY]]), ptr %[[HANDLE_PTR]]
+// SPIRV: %[[HANDLE:.*]] = load target("spirv.SignedImage", i32, 1, 2, [[ARRAYED]], [[MS]], [[SAMPLED]], [[IMG_FMT]]), ptr %[[HANDLE_PTR]]
 // CHECK: %[[INDEX_VAL:.*]] = load <[[COORD_DIM]] x i32>, ptr %[[INDEX_ADDR]]
-// DXIL: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.{{.*}}(target("dx.Texture", <3 x i32>, [[RW]], 0, 1, [[DXIL_TY]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
-// SPIRV: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.{{.*}}(target("spirv.SignedImage", i32, 1, 2, [[ARRAYED]], 0, [[SAMPLED]], [[IMG_FMT]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
+// DXIL: %[[PTR:.*]] = call ptr @llvm.dx.resource.getpointer.p0.{{.*}}(target("[[DXIL_HANDLE]]", <3 x i32>, [[RW]], [[ROV_OR_COUNT]], 1, [[DXIL_TY]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
+// SPIRV: %[[PTR:.*]] = call ptr addrspace(11) @llvm.spv.resource.getpointer.p11.{{.*}}(target("spirv.SignedImage", i32, 1, 2, [[ARRAYED]], [[MS]], [[SAMPLED]], [[IMG_FMT]]) %[[HANDLE]], <[[COORD_DIM]] x i32> %[[INDEX_VAL]])
 // CHECK: ret ptr {{.*}}%[[PTR]]
diff --git a/clang/test/SemaHLSL/Resources/MultiSampledTextures-Sema.hlsl b/clang/test/SemaHLSL/Resources/MultiSampledTextures-Sema.hlsl
new file mode 100644
index 0000000000000..40236507d85e2
--- /dev/null
+++ b/clang/test/SemaHLSL/Resources/MultiSampledTextures-Sema.hlsl
@@ -0,0 +1,55 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -finclude-default-header -fsyntax-only -verify -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -DINDEX_TYPE=uint2 %s
+
+// A multisampled texture supports the sample-indexed Load and operator[], but
+// does not support Sample/SampleBias/SampleGrad/SampleLevel/SampleCmp, Gather,
+// CalculateLevelOfDetail, or mips.
+
+TEXTURE<float4> T;
+SamplerState S;
+
+// The sample_count template parameter is a non-type 'int' parameter, so it must
+// be an integer constant expression. There is currently no HLSL-specific
+// validation of the value itself (0, negative, non-power-of-2, and out-of-range
+// counts are all accepted); the value is forwarded to the generated resource
+// type as the multisampled sample count.
+TEXTURE<float4, 4> TExplicit;       // explicit valid count
+static const int SC = 8;
+TEXTURE<float4, SC> TConst;         // constant-expression count
+
+int NonConst;
+TEXTURE<float4, NonConst> TBad;     // expected-error {{non-type template argument is not a constant expression}}
+// expected-note at -1 {{read of non-const variable 'NonConst' is not allowed in a constant expression}}
+// expected-note at -3 {{declared here}}
+TEXTURE<float4, 1.5> TFloat;        // expected-error {{conversion from 'float' to 'int' is not allowed in a converted constant expression}}
+
+// The element type has no default argument: a multisampled texture must be
+// declared with an explicit element type, matching DXC. Both the bare
+// template-name (shorthand) form and an empty template argument list are
+// errors. (Non-multisampled textures such as Texture2D do allow these forms.)
+TEXTURE TBare;                      // expected-error-re {{use of class template 'Texture2DMS{{(Array)?}}' requires template arguments}}
+// expected-note@*:* {{template declaration from hidden source}}
+TEXTURE<> TEmpty;                   // expected-error-re {{too few template arguments for class template 'Texture2DMS{{(Array)?}}'}}
+// expected-note@*:* {{template declaration from hidden source}}
+
+void valid() {
+  // Sample-indexed Load and its offset overload.
+  float4 a = T.Load((LOCATION_TYPE)0, 0);
+  float4 b = T.Load((LOCATION_TYPE)0, 0, int2(1, 1));
+  // operator[] reads sample 0.
+  float4 c = T[(INDEX_TYPE)0];
+}
+
+void unsupported() {
+  T.Sample(S, float2(0, 0));   // expected-error-re {{no member named 'Sample' in 'hlsl::Texture2DMS{{(Array)?}}<vector<float, 4>>'}}
+  T.SampleLevel(S, float2(0, 0), 0); // expected-error-re {{no member named 'SampleLevel' in 'hlsl::Texture2DMS{{(Array)?}}<vector<float, 4>>'}}
+  T.Gather(S, float2(0, 0));   // expected-error-re {{no member named 'Gather' in 'hlsl::Texture2DMS{{(Array)?}}<vector<float, 4>>'}}
+  T.CalculateLevelOfDetail(S, float2(0, 0)); // expected-error-re {{no member named 'CalculateLevelOfDetail' in 'hlsl::Texture2DMS{{(Array)?}}<vector<float, 4>>'}}
+  T.mips[0][(LOCATION_TYPE)0]; // expected-error-re {{no member named 'mips' in 'hlsl::Texture2DMS{{(Array)?}}<vector<float, 4>>'}}
+}
+
+void bad_load() {
+  // Load on a multisampled texture requires a sample index.
+  T.Load((LOCATION_TYPE)0);    // expected-error {{no matching member function for call to 'Load'}}
+  // expected-note@*:* {{candidate function not viable: requires 2 arguments, but 1 was provided}}
+  // expected-note@*:* {{candidate function not viable: requires 3 arguments, but 1 was provided}}
+}
diff --git a/llvm/include/llvm/IR/IntrinsicsDirectX.td b/llvm/include/llvm/IR/IntrinsicsDirectX.td
index 4dd86270f0d01..81f390cc4fd8e 100644
--- a/llvm/include/llvm/IR/IntrinsicsDirectX.td
+++ b/llvm/include/llvm/IR/IntrinsicsDirectX.td
@@ -134,6 +134,12 @@ def int_dx_resource_load_level
                              llvm_any_ty],
                             [IntrReadMem]>;
 
+def int_dx_resource_load_ms
+    : DefaultAttrsIntrinsic<[llvm_any_ty],
+                            [llvm_any_ty, llvm_any_ty, llvm_i32_ty,
+                             llvm_any_ty],
+                            [IntrReadMem]>;
+
 def int_dx_resource_calculate_lod
     : DefaultAttrsIntrinsic<[llvm_float_ty],
                             [llvm_any_ty, llvm_any_ty, llvm_any_ty],
diff --git a/llvm/include/llvm/IR/IntrinsicsSPIRV.td b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
index d948ef78b9584..448f9664fc545 100644
--- a/llvm/include/llvm/IR/IntrinsicsSPIRV.td
+++ b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
@@ -290,6 +290,12 @@ def int_spv_rsqrt : DefaultAttrsIntrinsic<[LLVMMatchType<0>], [llvm_anyfloat_ty]
                                llvm_any_ty],
                               [IntrReadMem]>;
 
+  def int_spv_resource_load_ms
+      : DefaultAttrsIntrinsic<[llvm_any_ty],
+                              [llvm_any_ty, llvm_any_ty, llvm_i32_ty,
+                               llvm_any_ty],
+                              [IntrReadMem]>;
+
   def int_spv_resource_samplecmp
       : DefaultAttrsIntrinsic<[llvm_any_ty],
                               [llvm_any_ty, llvm_any_ty, llvm_any_ty,

>From d8f6aeb2f5955cacf075a6bf10a41144a9fa26f8 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Tue, 28 Jul 2026 14:56:25 -0700
Subject: [PATCH 2/6] Remove unecessary -pch test

---
 .../resources/MultiSampledTextures-pch.hlsl   | 23 -------------------
 1 file changed, 23 deletions(-)
 delete mode 100644 clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl

diff --git a/clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl b/clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl
deleted file mode 100644
index 7e3b5b981648c..0000000000000
--- a/clang/test/CodeGenHLSL/resources/MultiSampledTextures-pch.hlsl
+++ /dev/null
@@ -1,23 +0,0 @@
-// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -emit-pch -o %t %s
-// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -finclude-default-header -DTEXTURE=Texture2DMS -DLOCATION_TYPE=int2 -include-pch %t -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s -DTEXTURE=Texture2DMS -DKIND=3
-
-// The Texture2DMS<T, N> sample count is part of the resource handle type, so it
-// has to survive serialization: a deserialized handle still lowers to a
-// dx.MSTexture with sample count 4.
-
-#ifndef HEADER
-#define HEADER
-
-TEXTURE<float4, 4> TMS4;
-
-#else
-
-// CHECK: %"class.hlsl::[[TEXTURE]]" = type { target("dx.MSTexture", <4 x float>, 0, 4, 0, [[KIND]]) }
-
-[numthreads(1, 1, 1)]
-void main() {
-  float4 V = TMS4.Load((LOCATION_TYPE)0, 0);
-  (void)V;
-}
-
-#endif

>From 6fc08d07032b46ff69c23db81235aa1d2dbf9240 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Tue, 28 Jul 2026 14:57:44 -0700
Subject: [PATCH 3/6] Add sema test for the sample count template parameter

---
 .../SampleCount-template-arg-errors.hlsl      | 74 +++++++++++++++++++
 1 file changed, 74 insertions(+)
 create mode 100644 clang/test/SemaHLSL/Resources/SampleCount-template-arg-errors.hlsl

diff --git a/clang/test/SemaHLSL/Resources/SampleCount-template-arg-errors.hlsl b/clang/test/SemaHLSL/Resources/SampleCount-template-arg-errors.hlsl
new file mode 100644
index 0000000000000..96c5f1ba45529
--- /dev/null
+++ b/clang/test/SemaHLSL/Resources/SampleCount-template-arg-errors.hlsl
@@ -0,0 +1,74 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -finclude-default-header -fsyntax-only -verify %s
+
+// The `sample_count` non-type template parameter only exists on multisampled
+// texture types. Every other resource type takes just an `element_type`, so
+// providing a second template argument must be diagnosed as too many template
+// arguments.
+
+struct S {
+  float4 F;
+};
+
+// Multisampled textures accept the sample count.
+Texture2DMS<float4> MSDefaultCount;
+Texture2DMS<float4, 4> MSExplicitCount;
+
+// ...but not more than that.
+Texture2DMS<float4, 4, 8> MSTooMany;
+// expected-error at -1 {{too many template arguments for class template 'Texture2DMS'}}
+// expected-note@*:* {{template declaration from hidden source}}
+
+// Typed buffers.
+Buffer<float4, 4> B;
+// expected-error at -1 {{too many template arguments for class template 'Buffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+RWBuffer<float4, 4> RWB;
+// expected-error at -1 {{too many template arguments for class template 'RWBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+RasterizerOrderedBuffer<float4, 4> ROB;
+// expected-error at -1 {{too many template arguments for class template 'RasterizerOrderedBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+
+// Structured buffers.
+StructuredBuffer<S, 4> SB;
+// expected-error at -1 {{too many template arguments for class template 'StructuredBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+RWStructuredBuffer<S, 4> RWSB;
+// expected-error at -1 {{too many template arguments for class template 'RWStructuredBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+AppendStructuredBuffer<S, 4> ASB;
+// expected-error at -1 {{too many template arguments for class template 'AppendStructuredBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+ConsumeStructuredBuffer<S, 4> CSB;
+// expected-error at -1 {{too many template arguments for class template 'ConsumeStructuredBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+RasterizerOrderedStructuredBuffer<S, 4> ROSB;
+// expected-error at -1 {{too many template arguments for class template 'RasterizerOrderedStructuredBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+
+// Constant buffers.
+ConstantBuffer<S, 4> CB;
+// expected-error at -1 {{too many template arguments for class template 'ConstantBuffer'}}
+// expected-note@*:* {{template declaration from hidden source}}
+
+// Non-multisampled textures.
+Texture2D<float4, 4> T2D;
+// expected-error at -1 {{too many template arguments for class template 'Texture2D'}}
+// expected-note@*:* {{template declaration from hidden source}}
+RWTexture2D<float4, 4> RWT2D;
+// expected-error at -1 {{too many template arguments for class template 'RWTexture2D'}}
+// expected-note@*:* {{template declaration from hidden source}}
+Texture2DArray<float4, 4> T2DA;
+// expected-error at -1 {{too many template arguments for class template 'Texture2DArray'}}
+// expected-note@*:* {{template declaration from hidden source}}
+RWTexture2DArray<float4, 4> RWT2DA;
+// expected-error at -1 {{too many template arguments for class template 'RWTexture2DArray'}}
+// expected-note@*:* {{template declaration from hidden source}}
+
+// Resource types that are not templates at all cannot take a sample count
+// either.
+ByteAddressBuffer<4> BAB;      // expected-error {{expected unqualified-id}}
+RWByteAddressBuffer<4> RWBAB;  // expected-error {{expected unqualified-id}}
+RasterizerOrderedByteAddressBuffer<4> ROBAB; // expected-error {{expected unqualified-id}}
+SamplerState<4> Samp;          // expected-error {{expected unqualified-id}}
+SamplerComparisonState<4> SampCmp; // expected-error {{expected unqualified-id}}

>From 4703ca6a4b8a652f9310e74b310d431ef0c8b6d2 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Tue, 18 Aug 2026 11:47:42 -0700
Subject: [PATCH 4/6] Remove IsMultiSampled field and fix
 ASTStructuralEquivalence logic with SampleCountExpr

---
 clang/include/clang/AST/TypeBase.h            | 23 ++++++-------
 clang/include/clang/AST/TypeProperties.td     |  5 +--
 clang/lib/AST/ASTStructuralEquivalence.cpp    | 15 +++++++--
 clang/lib/AST/ItaniumMangle.cpp               |  2 +-
 clang/lib/AST/Type.cpp                        |  1 -
 clang/lib/AST/TypePrinter.cpp                 |  2 +-
 clang/lib/CodeGen/Targets/DirectX.cpp         |  4 +--
 clang/lib/CodeGen/Targets/SPIR.cpp            |  2 +-
 clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp | 33 +++++++------------
 clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h   |  5 ++-
 clang/lib/Sema/HLSLExternalSemaSource.cpp     | 11 ++++++-
 clang/lib/Sema/SemaHLSL.cpp                   | 13 +++++---
 12 files changed, 63 insertions(+), 53 deletions(-)

diff --git a/clang/include/clang/AST/TypeBase.h b/clang/include/clang/AST/TypeBase.h
index 791b014779a05..aae5e9f82635d 100644
--- a/clang/include/clang/AST/TypeBase.h
+++ b/clang/include/clang/AST/TypeBase.h
@@ -6882,37 +6882,37 @@ class HLSLAttributedResourceType : public Type, public llvm::FoldingSetNode {
     LLVM_PREFERRED_TYPE(bool)
     uint8_t IsArray : 1;
 
-    LLVM_PREFERRED_TYPE(bool)
-    uint8_t IsMultiSampled : 1;
-
-    /// The N in Texture2DMS<T, N>; null for every other resource.
+    /// The N in Texture2DMS<T, N>; null for every resource that is not
+    /// multisampled. A multisampled resource always carries a sample count,
+    /// defaulting to 0, which means the count comes from the bound resource
+    /// at runtime rather than denoting zero samples.
     Expr *SampleCountExpr;
 
     Attributes(llvm::dxil::ResourceClass ResourceClass,
                llvm::dxil::ResourceDimension ResourceDimension,
                bool IsROV = false, bool RawBuffer = false,
                bool IsCounter = false, bool IsArray = false,
-               bool IsMultiSampled = false, Expr *SampleCountExpr = nullptr)
+               Expr *SampleCountExpr = nullptr)
         : ResourceClass(ResourceClass), ResourceDimension(ResourceDimension),
           IsROV(IsROV), RawBuffer(RawBuffer), IsCounter(IsCounter),
-          IsArray(IsArray), IsMultiSampled(IsMultiSampled),
-          SampleCountExpr(SampleCountExpr) {}
+          IsArray(IsArray), SampleCountExpr(SampleCountExpr) {}
 
     Attributes(llvm::dxil::ResourceClass ResourceClass)
         : Attributes(ResourceClass, llvm::dxil::ResourceDimension::Unknown) {}
 
     Attributes()
         : Attributes(llvm::dxil::ResourceClass::UAV,
-                     llvm::dxil::ResourceDimension::Unknown, false, false,
-                     false, false, false) {}
+                     llvm::dxil::ResourceDimension::Unknown) {}
+
+    bool isMultiSampled() const { return SampleCountExpr != nullptr; }
 
     friend bool operator==(const Attributes &LHS, const Attributes &RHS) {
       return std::tie(LHS.ResourceClass, LHS.ResourceDimension, LHS.IsROV,
                       LHS.RawBuffer, LHS.IsCounter, LHS.IsArray,
-                      LHS.IsMultiSampled, LHS.SampleCountExpr) ==
+                      LHS.SampleCountExpr) ==
              std::tie(RHS.ResourceClass, RHS.ResourceDimension, RHS.IsROV,
                       RHS.RawBuffer, RHS.IsCounter, RHS.IsArray,
-                      RHS.IsMultiSampled, RHS.SampleCountExpr);
+                      RHS.SampleCountExpr);
     }
     friend bool operator!=(const Attributes &LHS, const Attributes &RHS) {
       return !(LHS == RHS);
@@ -6938,6 +6938,7 @@ class HLSLAttributedResourceType : public Type, public llvm::FoldingSetNode {
   QualType getContainedType() const { return ContainedType; }
   bool hasContainedType() const { return !ContainedType.isNull(); }
   Expr *getSampleCountExpr() const { return Attrs.SampleCountExpr; }
+  bool isMultiSampled() const { return Attrs.isMultiSampled(); }
   const Attributes &getAttrs() const { return Attrs; }
   bool isRaw() const { return Attrs.RawBuffer; }
   bool isStructured() const { return !ContainedType->isChar8Type(); }
diff --git a/clang/include/clang/AST/TypeProperties.td b/clang/include/clang/AST/TypeProperties.td
index 55fafa3acfa4a..054e7a7357970 100644
--- a/clang/include/clang/AST/TypeProperties.td
+++ b/clang/include/clang/AST/TypeProperties.td
@@ -693,9 +693,6 @@ let Class = HLSLAttributedResourceType in {
   def : Property<"isArray", Bool> {
     let Read = [{ node->getAttrs().IsArray }];
   }
-  def : Property<"isMultiSampled", Bool> {
-    let Read = [{ node->getAttrs().IsMultiSampled }];
-  }
   def : Property<"sampleCountExpr", Optional<ExprRef>> {
     let Read = [{ makeOptionalFromPointer(node->getSampleCountExpr()) }];
   }
@@ -703,7 +700,7 @@ let Class = HLSLAttributedResourceType in {
     HLSLAttributedResourceType::Attributes attrs(
         static_cast<llvm::dxil::ResourceClass>(resClass),
         static_cast<llvm::dxil::ResourceDimension>(resDimension), isROV,
-        rawBuffer, isCounter, isArray, isMultiSampled,
+        rawBuffer, isCounter, isArray,
         makePointerFromOptional(sampleCountExpr));
     return ctx.getHLSLAttributedResourceType(wrappedTy, containedTy, attrs);
   }]>;
diff --git a/clang/lib/AST/ASTStructuralEquivalence.cpp b/clang/lib/AST/ASTStructuralEquivalence.cpp
index d8bbfbe5dac72..0ebb3c0d55974 100644
--- a/clang/lib/AST/ASTStructuralEquivalence.cpp
+++ b/clang/lib/AST/ASTStructuralEquivalence.cpp
@@ -1238,9 +1238,18 @@ bool ASTStructuralEquivalence::isEquivalent(
             Context, cast<HLSLAttributedResourceType>(T1)->getContainedType(),
             cast<HLSLAttributedResourceType>(T2)->getContainedType()))
       return false;
-    if (cast<HLSLAttributedResourceType>(T1)->getAttrs() !=
-        cast<HLSLAttributedResourceType>(T2)->getAttrs())
-      return false;
+    {
+      const auto *Res1 = cast<HLSLAttributedResourceType>(T1);
+      const auto *Res2 = cast<HLSLAttributedResourceType>(T2);
+      if (!IsStructurallyEquivalent(Context, Res1->getSampleCountExpr(),
+                                    Res2->getSampleCountExpr()))
+        return false;
+      HLSLAttributedResourceType::Attributes Attrs1 = Res1->getAttrs();
+      HLSLAttributedResourceType::Attributes Attrs2 = Res2->getAttrs();
+      Attrs1.SampleCountExpr = Attrs2.SampleCountExpr = nullptr;
+      if (Attrs1 != Attrs2)
+        return false;
+    }
     break;
 
   case Type::HLSLInlineSpirv:
diff --git a/clang/lib/AST/ItaniumMangle.cpp b/clang/lib/AST/ItaniumMangle.cpp
index f8e6b898be250..40fff45cbe781 100644
--- a/clang/lib/AST/ItaniumMangle.cpp
+++ b/clang/lib/AST/ItaniumMangle.cpp
@@ -4691,7 +4691,7 @@ void CXXNameMangler::mangleType(const HLSLAttributedResourceType *T) {
     Str += "_Counter";
   if (Attrs.IsArray)
     Str += "_Array";
-  if (Attrs.IsMultiSampled)
+  if (Attrs.isMultiSampled())
     Str += "_MS";
   if (T->hasContainedType())
     Str += "_CT";
diff --git a/clang/lib/AST/Type.cpp b/clang/lib/AST/Type.cpp
index db3c94e13b7bc..6c33e25f5d513 100644
--- a/clang/lib/AST/Type.cpp
+++ b/clang/lib/AST/Type.cpp
@@ -5982,7 +5982,6 @@ void HLSLAttributedResourceType::Profile(llvm::FoldingSetNodeID &ID,
   ID.AddBoolean(Attrs.RawBuffer);
   ID.AddBoolean(Attrs.IsCounter);
   ID.AddBoolean(Attrs.IsArray);
-  ID.AddBoolean(Attrs.IsMultiSampled);
   ID.AddBoolean(Attrs.SampleCountExpr != nullptr);
   if (Attrs.SampleCountExpr)
     Attrs.SampleCountExpr->Profile(ID, Ctx, /*Canonical=*/true);
diff --git a/clang/lib/AST/TypePrinter.cpp b/clang/lib/AST/TypePrinter.cpp
index 60964d2859790..a7ceec29f7158 100644
--- a/clang/lib/AST/TypePrinter.cpp
+++ b/clang/lib/AST/TypePrinter.cpp
@@ -2213,7 +2213,7 @@ void TypePrinter::printHLSLAttributedResourceAfter(
     OS << " [[hlsl::is_counter]]";
   if (Attrs.IsArray)
     OS << " [[hlsl::is_array]]";
-  if (Attrs.IsMultiSampled)
+  if (Attrs.isMultiSampled())
     OS << " [[hlsl::is_ms]]";
 
   QualType ContainedTy = T->getContainedType();
diff --git a/clang/lib/CodeGen/Targets/DirectX.cpp b/clang/lib/CodeGen/Targets/DirectX.cpp
index 29998f6dfb16f..6240a64b6a539 100644
--- a/clang/lib/CodeGen/Targets/DirectX.cpp
+++ b/clang/lib/CodeGen/Targets/DirectX.cpp
@@ -71,7 +71,7 @@ llvm::Type *DirectXTargetCodeGenInfo::getHLSLType(
         ResAttrs.ResourceDimension != llvm::dxil::ResourceDimension::Unknown;
     assert((!IsRawBuffer || !IsTexture) && "A resource cannot be both a raw "
                                            "buffer and a texture.");
-    bool IsMultiSampledTexture = IsTexture && ResAttrs.IsMultiSampled;
+    bool IsMultiSampledTexture = IsTexture && ResAttrs.isMultiSampled();
     llvm::StringRef TypeName = "dx.TypedBuffer";
     if (IsRawBuffer)
       TypeName = "dx.RawBuffer";
@@ -112,7 +112,7 @@ llvm::Type *DirectXTargetCodeGenInfo::getHLSLType(
         RK = llvm::dxil::ResourceKind::Texture1D;
         break;
       case llvm::dxil::ResourceDimension::Dim2D:
-        if (ResAttrs.IsMultiSampled)
+        if (ResAttrs.isMultiSampled())
           RK = ResAttrs.IsArray ? llvm::dxil::ResourceKind::Texture2DMSArray
                                 : llvm::dxil::ResourceKind::Texture2DMS;
         else
diff --git a/clang/lib/CodeGen/Targets/SPIR.cpp b/clang/lib/CodeGen/Targets/SPIR.cpp
index dba7d4fa73923..152d6214526df 100644
--- a/clang/lib/CodeGen/Targets/SPIR.cpp
+++ b/clang/lib/CodeGen/Targets/SPIR.cpp
@@ -936,7 +936,7 @@ llvm::Type *CommonSPIRTargetCodeGenInfo::getSPIRVImageTypeFromHLSLResource(
   IntParams[2] = static_cast<unsigned>(attributes.IsArray);
 
   // MS
-  IntParams[3] = static_cast<unsigned>(attributes.IsMultiSampled);
+  IntParams[3] = static_cast<unsigned>(attributes.isMultiSampled());
 
   // Sampled
   IntParams[4] =
diff --git a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
index 7b778bd362d58..70ece482ae2e7 100644
--- a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
+++ b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
@@ -1065,8 +1065,7 @@ BuiltinTypeDeclBuilder::addBufferHandles(ResourceClass RC, bool IsROV,
                                          AccessSpecifier Access) {
   QualType ElementTy = getHandleElementType();
   addHandleMember(RC, ResourceDimension::Unknown, IsROV, RawBuffer,
-                  /*IsArray=*/false, ElementTy, /*IsMultiSampled=*/false,
-                  Access);
+                  /*IsArray=*/false, ElementTy, Access);
   if (HasCounter)
     addCounterHandleMember(RC, IsROV, RawBuffer, ElementTy, Access);
   return *this;
@@ -1074,9 +1073,10 @@ BuiltinTypeDeclBuilder::addBufferHandles(ResourceClass RC, bool IsROV,
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addTextureHandle(
     ResourceClass RC, bool IsROV, bool IsArray, ResourceDimension RD,
-    bool IsMultiSampled, AccessSpecifier Access) {
-  addHandleMember(RC, RD, IsROV, /*RawBuffer=*/false, IsArray,
-                  getHandleElementType(), IsMultiSampled, Access);
+    Expr *SampleCountExpr, AccessSpecifier Access) {
+  addResourceMember("__handle", RC, RD, IsROV, /*RawBuffer=*/false,
+                    /*IsCounter=*/false, IsArray, getHandleElementType(),
+                    SampleCountExpr, Access);
   return *this;
 }
 
@@ -1138,11 +1138,10 @@ CXXRecordDecl *BuiltinTypeDeclBuilder::addPrivateNestedRecord(StringRef Name) {
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addHandleMember(
     ResourceClass RC, ResourceDimension RD, bool IsROV, bool RawBuffer,
-    bool IsArray, QualType ElementTy, bool IsMultiSampled,
-    AccessSpecifier Access) {
+    bool IsArray, QualType ElementTy, AccessSpecifier Access) {
   return addResourceMember("__handle", RC, RD, IsROV, RawBuffer,
                            /*IsCounter=*/false, IsArray, ElementTy,
-                           IsMultiSampled, Access);
+                           /*SampleCountExpr=*/nullptr, Access);
 }
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addCounterHandleMember(
@@ -1151,13 +1150,13 @@ BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addCounterHandleMember(
   return addResourceMember("__counter_handle", RC, ResourceDimension::Unknown,
                            IsROV, RawBuffer, /*IsCounter=*/true,
                            /*IsArray=*/false, ElementTy,
-                           /*IsMultiSampled=*/false, Access);
+                           /*SampleCountExpr=*/nullptr, Access);
 }
 
 BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addResourceMember(
     StringRef MemberName, ResourceClass RC, ResourceDimension RD, bool IsROV,
     bool RawBuffer, bool IsCounter, bool IsArray, QualType ElementTy,
-    bool IsMultiSampled, AccessSpecifier Access) {
+    Expr *SampleCountExpr, AccessSpecifier Access) {
   assert(!Record->isCompleteDefinition() && "record is already complete");
 
   ASTContext &Ctx = SemaRef.getASTContext();
@@ -1183,16 +1182,8 @@ BuiltinTypeDeclBuilder &BuiltinTypeDeclBuilder::addResourceMember(
     Attrs.push_back(HLSLIsCounterAttr::CreateImplicit(Ctx));
   if (IsArray)
     Attrs.push_back(HLSLIsArrayAttr::CreateImplicit(Ctx));
-  Expr *SampleCountExpr = nullptr;
-  if (IsMultiSampled) {
+  if (SampleCountExpr)
     Attrs.push_back(HLSLIsMultiSampledAttr::CreateImplicit(Ctx));
-    ClassTemplateDecl *CTD = Record->getDescribedClassTemplate();
-    assert(CTD && "multisampled texture must be a class template");
-    auto *NTTP = cast<NonTypeTemplateParmDecl>(
-        CTD->getTemplateParameters()->getParam(1));
-    SampleCountExpr = SemaRef.BuildDeclRefExpr(NTTP, NTTP->getType(),
-                                               VK_PRValue, SourceLocation());
-  }
 
   if (CreateHLSLAttributedResourceType(SemaRef, Ctx.HLSLResourceTy, Attrs,
                                        AttributedResTy, /*LocInfo=*/nullptr,
@@ -1506,7 +1497,7 @@ CXXRecordDecl *BuiltinTypeDeclBuilder::addMipsSliceType(ResourceDimension Dim,
       .addHandleMember(getResourceAttrs().ResourceClass, Dim,
                        getResourceAttrs().IsROV, /*RawBuffer=*/false,
                        getResourceAttrs().IsArray, ReturnType,
-                       /*IsMultiSampled=*/false, AccessSpecifier::AS_public)
+                       AccessSpecifier::AS_public)
       .addMemberVariable("__level", IntTy, {}, AccessSpecifier::AS_public)
       .addDefaultHandleConstructor(AccessSpecifier::AS_protected)
       .addCopyConstructor(AccessSpecifier::AS_protected)
@@ -1550,7 +1541,7 @@ CXXRecordDecl *BuiltinTypeDeclBuilder::addMipsType(ResourceDimension Dim,
       .addHandleMember(getResourceAttrs().ResourceClass, Dim,
                        getResourceAttrs().IsROV, /*RawBuffer=*/false,
                        getResourceAttrs().IsArray, ReturnType,
-                       /*IsMultiSampled=*/false, AccessSpecifier::AS_public)
+                       AccessSpecifier::AS_public)
       .addDefaultHandleConstructor(AccessSpecifier::AS_protected)
       .addCopyConstructor(AccessSpecifier::AS_protected)
       .addCopyAssignmentOperator(AccessSpecifier::AS_protected);
diff --git a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h
index 2b527765ce527..a7521cd8480fd 100644
--- a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h
+++ b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.h
@@ -87,7 +87,7 @@ class BuiltinTypeDeclBuilder {
                    AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &
   addTextureHandle(ResourceClass RC, bool IsROV, bool IsArray,
-                   ResourceDimension RD, bool IsMultiSampled = false,
+                   ResourceDimension RD, Expr *SampleCountExpr = nullptr,
                    AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &addSamplerHandle();
   BuiltinTypeDeclBuilder &addConstantBufferConversionToType();
@@ -162,7 +162,7 @@ class BuiltinTypeDeclBuilder {
   addResourceMember(StringRef MemberName, ResourceClass RC,
                     ResourceDimension RD, bool IsROV, bool RawBuffer,
                     bool IsCounter, bool IsArray, QualType ElementTy,
-                    bool IsMultiSampled = false,
+                    Expr *SampleCountExpr = nullptr,
                     AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &addFriend(CXXRecordDecl *Friend);
   CXXRecordDecl *addPrivateNestedRecord(StringRef Name);
@@ -171,7 +171,6 @@ class BuiltinTypeDeclBuilder {
   BuiltinTypeDeclBuilder &
   addHandleMember(ResourceClass RC, ResourceDimension RD, bool IsROV,
                   bool RawBuffer, bool IsArray, QualType ElementTy,
-                  bool IsMultiSampled = false,
                   AccessSpecifier Access = AccessSpecifier::AS_private);
   BuiltinTypeDeclBuilder &
   addCounterHandleMember(ResourceClass RC, bool IsROV, bool RawBuffer,
diff --git a/clang/lib/Sema/HLSLExternalSemaSource.cpp b/clang/lib/Sema/HLSLExternalSemaSource.cpp
index c666134b83304..18822ce7f92b1 100644
--- a/clang/lib/Sema/HLSLExternalSemaSource.cpp
+++ b/clang/lib/Sema/HLSLExternalSemaSource.cpp
@@ -302,9 +302,18 @@ static BuiltinTypeDeclBuilder setupRWTextureType(CXXRecordDecl *Decl, Sema &S,
 static BuiltinTypeDeclBuilder setupMSTextureType(CXXRecordDecl *Decl, Sema &S,
                                                  bool IsArray,
                                                  ResourceDimension Dim) {
+  ClassTemplateDecl *CTD = Decl->getDescribedClassTemplate();
+  assert(CTD && "multisampled texture must be a class template");
+
+  // Parameter 1 is the N in Texture2DMS<T, N>.
+  auto *NTTP =
+      cast<NonTypeTemplateParmDecl>(CTD->getTemplateParameters()->getParam(1));
+  Expr *SampleCountExpr =
+      S.BuildDeclRefExpr(NTTP, NTTP->getType(), VK_PRValue, SourceLocation());
+
   return BuiltinTypeDeclBuilder(S, Decl)
       .addTextureHandle(ResourceClass::SRV, /*IsROV=*/false, IsArray, Dim,
-                        /*IsMultiSampled=*/true)
+                        SampleCountExpr)
       .addTextureLoadMSMethods(Dim, IsArray)
       .addArraySubscriptOperators(Dim, IsArray)
       // TODO: Add MS-specific GetDimensions (with a NumberOfSamples output);
diff --git a/clang/lib/Sema/SemaHLSL.cpp b/clang/lib/Sema/SemaHLSL.cpp
index b3126324fae6c..1615ca3129e51 100644
--- a/clang/lib/Sema/SemaHLSL.cpp
+++ b/clang/lib/Sema/SemaHLSL.cpp
@@ -2163,12 +2163,17 @@ bool clang::CreateHLSLAttributedResourceType(
       ResAttrs.IsArray = true;
       break;
     case attr::HLSLIsMultiSampled:
-      if (ResAttrs.IsMultiSampled) {
+      if (ResAttrs.SampleCountExpr) {
         S.Diag(A->getLocation(), diag::warn_duplicate_attribute_exact) << A;
         return false;
       }
-      ResAttrs.IsMultiSampled = true;
-      ResAttrs.SampleCountExpr = SampleCountExpr;
+      // A bare [[hlsl::is_ms]] carries no count, so default it to 0, the same
+      // value Texture2DMS<T> gets from its template parameter.
+      ResAttrs.SampleCountExpr =
+          SampleCountExpr
+              ? SampleCountExpr
+              : IntegerLiteral::Create(S.Context, llvm::APInt(32, 0),
+                                       S.Context.IntTy, A->getLocation());
       break;
     case attr::HLSLIsCounter:
       if (ResAttrs.IsCounter) {
@@ -3959,7 +3964,7 @@ static bool CheckLoadMSBuiltin(Sema &S, CallExpr *TheCall) {
   // Check the multisampled texture handle.
   if (CheckResourceHandle(&S, TheCall, 0,
                           [](const HLSLAttributedResourceType *ResType) {
-                            return !ResType->getAttrs().IsMultiSampled;
+                            return !ResType->isMultiSampled();
                           }))
     return true;
 

>From 789aa94228c8593474a2c1028a9d24a3baa62838 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Tue, 18 Aug 2026 18:20:15 -0700
Subject: [PATCH 5/6] Make texture load methods return *this like the buffer
 loads

---
 clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
index 70ece482ae2e7..4a9b7b1b56a64 100644
--- a/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
+++ b/clang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp
@@ -1612,12 +1612,14 @@ BuiltinTypeDeclBuilder::addTextureLoadMethods(ResourceDimension Dim,
       .finalize();
 
   // T Load(int3 location, int2 offset)
-  return BuiltinTypeMethodBuilder(*this, "Load", ReturnType)
+  BuiltinTypeMethodBuilder(*this, "Load", ReturnType)
       .addParam("Location", LocationTy)
       .addParam("Offset", OffsetTy)
       .callBuiltin("__builtin_hlsl_resource_load_level", ReturnType, PH::Handle,
                    PH::_0, PH::_1)
       .finalize();
+
+  return *this;
 }
 
 BuiltinTypeDeclBuilder &
@@ -1644,13 +1646,15 @@ BuiltinTypeDeclBuilder::addTextureLoadMSMethods(ResourceDimension Dim,
       .finalize();
 
   // T Load(int2 location, int sampleIndex, int2 offset)
-  return BuiltinTypeMethodBuilder(*this, "Load", ReturnType)
+  BuiltinTypeMethodBuilder(*this, "Load", ReturnType)
       .addParam("Location", LocationTy)
       .addParam("SampleIndex", IntTy)
       .addParam("Offset", OffsetTy)
       .callBuiltin("__builtin_hlsl_resource_load_ms", ReturnType, PH::Handle,
                    PH::_0, PH::_1, PH::_2)
       .finalize();
+
+  return *this;
 }
 
 BuiltinTypeDeclBuilder &

>From b15300b8fbb9a9dcc4f8e196941e2366f9f5d276 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Tue, 18 Aug 2026 18:29:18 -0700
Subject: [PATCH 6/6] Link appropriate issue for TODO on MS-specific
 GetDimensions

---
 clang/lib/Sema/HLSLExternalSemaSource.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/clang/lib/Sema/HLSLExternalSemaSource.cpp b/clang/lib/Sema/HLSLExternalSemaSource.cpp
index 18822ce7f92b1..d8a697041ae7d 100644
--- a/clang/lib/Sema/HLSLExternalSemaSource.cpp
+++ b/clang/lib/Sema/HLSLExternalSemaSource.cpp
@@ -318,6 +318,7 @@ static BuiltinTypeDeclBuilder setupMSTextureType(CXXRecordDecl *Decl, Sema &S,
       .addArraySubscriptOperators(Dim, IsArray)
       // TODO: Add MS-specific GetDimensions (with a NumberOfSamples output);
       // the generic addGetDimensionsMethods is mip-based and unsuitable here.
+      // https://github.com/llvm/wg-hlsl/issues/347
       .addDefaultHandleConstructor()
       .addCopyConstructor()
       .addCopyAssignmentOperator()



More information about the llvm-commits mailing list