spirv-reader: fix texel type for textureStore

Fixed: tint:381
Change-Id: I7521bac842b59c16c596e1cea39a14700a9e7f9a
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/50449
Commit-Queue: David Neto <dneto@google.com>
Reviewed-by: Alan Baker <alanbaker@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 1428e7a..778d1d9 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -4924,75 +4924,62 @@
     Fail();
     return nullptr;
   }
+
+  // The texel type is always a 4-element vector.
+  const uint32_t dest_count = 4u;
+  TINT_ASSERT(dest_type->Is<Vector>() &&
+              dest_type->As<Vector>()->size == dest_count);
+  TINT_ASSERT(dest_type->IsFloatVector() ||
+              dest_type->IsUnsignedIntegerVector() ||
+              dest_type->IsSignedIntegerVector());
+
   if (src_type == dest_type) {
     return texel.expr;
   }
 
-  const uint32_t dest_count =
-      dest_type->IsScalar() ? 1 : dest_type->As<Vector>()->size;
-  if (dest_count == 3) {
-    Fail() << "3-channel storage textures are not supported: "
+  // Component type must match floatness, or integral signedness.
+  if ((src_type->IsFloatScalarOrVector() != dest_type->IsFloatVector()) ||
+      (src_type->IsUnsignedIntegerVector() !=
+       dest_type->IsUnsignedIntegerVector()) ||
+      (src_type->IsSignedIntegerVector() !=
+       dest_type->IsSignedIntegerVector())) {
+    Fail() << "invalid texel type for storage texture write: component must be "
+              "float, signed integer, or unsigned integer "
+              "to match the texture channel type: "
            << inst.PrettyPrint();
     return nullptr;
   }
+
+  const auto required_count = parser_impl_.GetChannelCountForFormat(format);
+  TINT_ASSERT(0 < required_count && required_count <= 4);
+
   const uint32_t src_count =
       src_type->IsScalar() ? 1 : src_type->As<Vector>()->size;
-  if (src_count < dest_count) {
+  if (src_count < required_count) {
     Fail() << "texel has too few components for storage texture: " << src_count
-           << " provided but " << dest_count
+           << " provided but " << required_count
            << " required, in: " << inst.PrettyPrint();
     return nullptr;
   }
-  // If the texel has more components than necessary, then we will ignore the
-  // higher-numbered components.
-  auto* texel_prefix =
-      (src_count == dest_count)
-          ? texel.expr
-          : create<ast::MemberAccessorExpression>(Source{}, texel.expr,
-                                                  PrefixSwizzle(dest_count));
 
-  if (!(dest_type->IsFloatScalarOrVector() ||
-        dest_type->IsUnsignedScalarOrVector() ||
-        dest_type->IsSignedScalarOrVector())) {
-    Fail() << "invalid destination type for storage texture write: "
-           << dest_type->TypeInfo().name;
-    return nullptr;
-  }
-  if (!(src_type->IsFloatScalarOrVector() ||
-        src_type->IsUnsignedScalarOrVector() ||
-        src_type->IsSignedScalarOrVector())) {
-    Fail() << "invalid texel type for storage texture write: "
-           << inst.PrettyPrint();
-    return nullptr;
-  }
-  if (dest_type->IsFloatScalarOrVector() &&
-      !src_type->IsFloatScalarOrVector()) {
-    Fail() << "can only write float or float vector to a storage image with "
-              "floating texel format: "
-           << inst.PrettyPrint();
-    return nullptr;
-  }
-  if (!dest_type->IsFloatScalarOrVector() &&
-      src_type->IsFloatScalarOrVector()) {
-    Fail()
-        << "float or float vector can only be written to a storage image with "
-           "floating texel format: "
-        << inst.PrettyPrint();
-    return nullptr;
+  // It's valid for required_count < src_count. The extra components will
+  // be written out but the textureStore will ignore them.
+
+  if (src_count < dest_count) {
+    // Expand the texel to a 4 element vector.
+    auto* component_type =
+        texel.type->IsScalar() ? texel.type : texel.type->As<Vector>()->type;
+    texel.type = ty_.Vector(component_type, dest_count);
+    ast::ExpressionList exprs;
+    exprs.push_back(texel.expr);
+    for (auto i = src_count; i < dest_count; i++) {
+      exprs.push_back(parser_impl_.MakeNullExpression(component_type).expr);
+    }
+    texel.expr = create<ast::TypeConstructorExpression>(
+        Source{}, texel.type->Build(builder_), std::move(exprs));
   }
 
-  if (dest_type->IsFloatScalarOrVector()) {
-    return texel_prefix;
-  }
-  // The only remaining cases are signed/unsigned source, and signed/unsigned
-  // destination.
-  if (dest_type->IsUnsignedScalarOrVector() ==
-      src_type->IsUnsignedScalarOrVector()) {
-    return texel_prefix;
-  }
-  // We must do a bitcast conversion.
-  return create<ast::BitcastExpression>(Source{}, dest_type->Build(builder_),
-                                        texel_prefix);
+  return texel.expr;
 }
 
 TypedExpression FunctionEmitter::ToI32(TypedExpression value) {
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index 64c2a93..58203d9 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -963,10 +963,10 @@
   bool EmitImageQuery(const spvtools::opt::Instruction& inst);
 
   /// Converts the given texel to match the type required for the storage
-  /// texture with the given type. This can generate a swizzle to retain
-  /// only the first few components of the texel vector, and maybe a bitcast
-  /// to convert signedness.  Returns an expression, or emits an error and
-  /// returns nullptr.
+  /// texture with the given type. In WGSL the texel value is always provided
+  /// as a 4-element vector, but the component type is determined by the
+  /// texel channel type. See "Texel Formats for Storage Textures" in the WGSL
+  /// spec. Returns an expression, or emits an error and returns nullptr.
   /// @param inst the image access instruction (used for diagnostics)
   /// @param texel the texel
   /// @param texture_type the type of the storage texture
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index c0f6a50..b19ef55 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -2168,12 +2168,7 @@
   return nullptr;
 }
 
-const Type* ParserImpl::GetTexelTypeForFormat(ast::ImageFormat format) {
-  auto* component_type = GetComponentTypeForFormat(format);
-  if (!component_type) {
-    return nullptr;
-  }
-
+unsigned ParserImpl::GetChannelCountForFormat(ast::ImageFormat format) {
   switch (format) {
     case ast::ImageFormat::kR16Float:
     case ast::ImageFormat::kR16Sint:
@@ -2186,7 +2181,7 @@
     case ast::ImageFormat::kR8Uint:
     case ast::ImageFormat::kR8Unorm:
       // One channel
-      return component_type;
+      return 1;
 
     case ast::ImageFormat::kRg11B10Float:
     case ast::ImageFormat::kRg16Float:
@@ -2200,7 +2195,7 @@
     case ast::ImageFormat::kRg8Uint:
     case ast::ImageFormat::kRg8Unorm:
       // Two channels
-      return ty_.Vector(component_type, 2);
+      return 2;
 
     case ast::ImageFormat::kBgra8Unorm:
     case ast::ImageFormat::kBgra8UnormSrgb:
@@ -2217,13 +2212,21 @@
     case ast::ImageFormat::kRgba8Unorm:
     case ast::ImageFormat::kRgba8UnormSrgb:
       // Four channels
-      return ty_.Vector(component_type, 4);
+      return 4;
 
     default:
       break;
   }
-  Fail() << "unknown format: " << int(format);
-  return nullptr;
+  Fail() << "unknown format " << int(format);
+  return 0;
+}
+
+const Type* ParserImpl::GetTexelTypeForFormat(ast::ImageFormat format) {
+  const auto* component_type = GetComponentTypeForFormat(format);
+  if (!component_type) {
+    return nullptr;
+  }
+  return ty_.Vector(component_type, 4);
 }
 
 bool ParserImpl::RegisterHandleUsage() {
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index 7253cdd..683df89 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -518,7 +518,14 @@
   /// @returns the component type, one of f32, i32, u32
   const Type* GetComponentTypeForFormat(ast::ImageFormat format);
 
-  /// Returns texel type corresponding to the given image format.
+  /// Returns the number of channels in the given image format.
+  /// @param format image texel format
+  /// @returns the number of channels in the format
+  unsigned GetChannelCountForFormat(ast::ImageFormat format);
+
+  /// Returns the texel type corresponding to the given image format.
+  /// This the WGSL type used for the texel parameter to textureStore.
+  /// It's always a 4-element vector.
   /// @param format image texel format
   /// @returns the texel format
   const Type* GetTexelTypeForFormat(ast::ImageFormat format);
diff --git a/src/reader/spirv/parser_impl_handle_test.cc b/src/reader/spirv/parser_impl_handle_test.cc
index aa5d7cb..c23b03d 100644
--- a/src/reader/spirv/parser_impl_handle_test.cc
+++ b/src/reader/spirv/parser_impl_handle_test.cc
@@ -1241,6 +1241,152 @@
     __access_control_write_only__storage_texture_1d_rg32float
   })"}));
 
+// Test handle declaration or error, when there is an image access.
+
+struct ImageDeclCase {
+  // SPIR-V image type, excluding result ID and opcode
+  std::string spirv_image_type_details;
+  std::string spirv_image_access;  // Optional instruction to provoke use
+  std::string expected_error;
+  std::string expected_decl;
+};
+
+inline std::ostream& operator<<(std::ostream& out, const ImageDeclCase& c) {
+  out << "ImageDeclCase(" << c.spirv_image_type_details << "\n"
+      << "access: " << c.spirv_image_access << "\n"
+      << "error: " << c.expected_error << "\n"
+      << "decl:" << c.expected_decl << "\n)";
+  return out;
+}
+
+using SpvParserHandleTest_ImageDeclTest =
+    SpvParserTestBase<::testing::TestWithParam<ImageDeclCase>>;
+
+TEST_P(SpvParserHandleTest_ImageDeclTest, DeclareAndUseHandle) {
+  // Only declare the sampled image type, and the associated variable
+  // if the requested image type is a sampled image type and not multisampled.
+  const bool is_sampled_image_type = GetParam().spirv_image_type_details.find(
+                                         "0 1 Unknown") != std::string::npos;
+  const auto assembly =
+      Preamble() + R"(
+     OpEntryPoint Fragment %100 "main"
+     OpExecutionMode %100 OriginUpperLeft
+     OpName %float_var "float_var"
+     OpName %ptr_float "ptr_float"
+     OpName %i1 "i1"
+     OpName %vi12 "vi12"
+     OpName %vi123 "vi123"
+     OpName %vi1234 "vi1234"
+     OpName %u1 "u1"
+     OpName %vu12 "vu12"
+     OpName %vu123 "vu123"
+     OpName %vu1234 "vu1234"
+     OpName %f1 "f1"
+     OpName %vf12 "vf12"
+     OpName %vf123 "vf123"
+     OpName %vf1234 "vf1234"
+     OpDecorate %10 DescriptorSet 0
+     OpDecorate %10 Binding 0
+     OpDecorate %20 DescriptorSet 2
+     OpDecorate %20 Binding 1
+     OpDecorate %30 DescriptorSet 0
+     OpDecorate %30 Binding 1
+)" + CommonBasicTypes() +
+      R"(
+     %sampler = OpTypeSampler
+     %ptr_sampler = OpTypePointer UniformConstant %sampler
+     %im_ty = OpTypeImage )" +
+      GetParam().spirv_image_type_details + R"(
+     %ptr_im_ty = OpTypePointer UniformConstant %im_ty
+)" + (is_sampled_image_type ? " %si_ty = OpTypeSampledImage %im_ty " : "") +
+      R"(
+
+     %ptr_float = OpTypePointer Function %float
+
+     %10 = OpVariable %ptr_sampler UniformConstant
+     %20 = OpVariable %ptr_im_ty UniformConstant
+     %30 = OpVariable %ptr_sampler UniformConstant ; comparison sampler, when needed
+
+     %100 = OpFunction %void None %voidfn
+     %entry = OpLabel
+
+     %float_var = OpVariable %ptr_float Function
+
+     %i1 = OpCopyObject %int %int_1
+     %vi12 = OpCopyObject %v2int %the_vi12
+     %vi123 = OpCopyObject %v3int %the_vi123
+     %vi1234 = OpCopyObject %v4int %the_vi1234
+
+     %u1 = OpCopyObject %uint %uint_1
+     %vu12 = OpCopyObject %v2uint %the_vu12
+     %vu123 = OpCopyObject %v3uint %the_vu123
+     %vu1234 = OpCopyObject %v4uint %the_vu1234
+
+     %f1 = OpCopyObject %float %float_1
+     %vf12 = OpCopyObject %v2float %the_vf12
+     %vf123 = OpCopyObject %v3float %the_vf123
+     %vf1234 = OpCopyObject %v4float %the_vf1234
+
+     %sam = OpLoad %sampler %10
+     %im = OpLoad %im_ty %20
+
+)" +
+      (is_sampled_image_type
+           ? " %sampled_image = OpSampledImage %si_ty %im %sam "
+           : "") +
+      GetParam().spirv_image_access +
+      R"(
+     ; Use an anchor for the cases when the image access doesn't have a result ID.
+     %1000 = OpCopyObject %uint %uint_0
+
+     OpReturn
+     OpFunctionEnd
+  )";
+  auto p = parser(test::Assemble(assembly));
+  const bool succeeded = p->BuildAndParseInternalModule();
+  if (succeeded) {
+    EXPECT_TRUE(GetParam().expected_error.empty());
+    const auto got = p->program().to_str();
+    EXPECT_THAT(got, HasSubstr(GetParam().expected_decl));
+  } else {
+    EXPECT_FALSE(GetParam().expected_error.empty());
+    EXPECT_THAT(p->error(), HasSubstr(GetParam().expected_error));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    Multisampled_Only2DNonArrayedIsValid,
+    SpvParserHandleTest_ImageDeclTest,
+    ::testing::ValuesIn(std::vector<ImageDeclCase>{
+        {"%float 1D 0 0 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
+         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
+        {"%float 1D 0 1 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
+         "WGSL arrayed textures must be 2d_array or cube_array: ", ""},
+        {"%float 2D 0 0 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
+         "", R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __multisampled_texture_2d__f32
+  }
+)"},
+        {"%float 2D 0 1 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
+         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
+        {"%float 3D 0 0 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
+         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
+        {"%float 3D 0 1 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
+         "WGSL arrayed textures must be 2d_array or cube_array: ", ""},
+        {"%float Cube 0 0 1 1 Unknown",
+         "%result = OpImageQuerySamples %uint %im",
+         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
+        {"%float Cube 0 1 1 1 Unknown",
+         "%result = OpImageQuerySamples %uint %im",
+         "WGSL multisampled textures must be 2d and non-arrayed: ", ""}}));
+
 // Test emission of variables when we have image accesses in executable code.
 
 struct ImageAccessCase {
@@ -2694,13 +2840,15 @@
     })"}}));
 
 INSTANTIATE_TEST_SUITE_P(
-    // SPIR-V's texel parameter is a 4-element vector with the component
-    // type matching the sampled type. WGSL's texel parameter might be
-    // scalar or vector, depending on the number of channels in the texture.
+    // SPIR-V's texel parameter is a scalar or vector with at least as many
+    // components as there are channels in the underlying format, and the
+    // componet type matches the sampled type (modulo signed/unsigned integer).
+    // WGSL's texel parameter is a 4-element vector scalar or vector, with
+    // component type equal to the 32-bit form of the channel type.
     ImageWrite_ConvertTexelOperand_Arity,
     SpvParserHandleTest_ImageAccessTest,
     ::testing::ValuesIn(std::vector<ImageAccessCase>{
-        // Source 1 component, dest 1 component
+        // Source 1 component
         {"%float 2D 0 0 0 2 R32f", "OpImageWrite %im %vi12 %f1",
          R"(
   Variable{
@@ -2717,7 +2865,13 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        Identifier[not set]{f1}
+        TypeConstructor[not set]{
+          __vec_4__f32
+          Identifier[not set]{f1}
+          ScalarConstructor[not set]{0.000000}
+          ScalarConstructor[not set]{0.000000}
+          ScalarConstructor[not set]{0.000000}
+        }
       )
     })"},
         // Source 2 component, dest 1 component
@@ -2737,9 +2891,11 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        MemberAccessor[not set]{
+        TypeConstructor[not set]{
+          __vec_4__f32
           Identifier[not set]{vf12}
-          Identifier[not set]{x}
+          ScalarConstructor[not set]{0.000000}
+          ScalarConstructor[not set]{0.000000}
         }
       )
     })"},
@@ -2760,9 +2916,10 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        MemberAccessor[not set]{
+        TypeConstructor[not set]{
+          __vec_4__f32
           Identifier[not set]{vf123}
-          Identifier[not set]{x}
+          ScalarConstructor[not set]{0.000000}
         }
       )
     })"},
@@ -2783,10 +2940,7 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        MemberAccessor[not set]{
-          Identifier[not set]{vf1234}
-          Identifier[not set]{x}
-        }
+        Identifier[not set]{vf1234}
       )
     })"},
         // Source 2 component, dest 2 component
@@ -2806,7 +2960,12 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        Identifier[not set]{vf12}
+        TypeConstructor[not set]{
+          __vec_4__f32
+          Identifier[not set]{vf12}
+          ScalarConstructor[not set]{0.000000}
+          ScalarConstructor[not set]{0.000000}
+        }
       )
     })"},
         // Source 3 component, dest 2 component
@@ -2826,9 +2985,10 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        MemberAccessor[not set]{
+        TypeConstructor[not set]{
+          __vec_4__f32
           Identifier[not set]{vf123}
-          Identifier[not set]{xy}
+          ScalarConstructor[not set]{0.000000}
         }
       )
     })"},
@@ -2849,10 +3009,7 @@
       (
         Identifier[not set]{x_20}
         Identifier[not set]{vi12}
-        MemberAccessor[not set]{
-          Identifier[not set]{vf1234}
-          Identifier[not set]{xy}
-        }
+        Identifier[not set]{vf1234}
       )
     })"},
         // WGSL does not support 3-component storage textures.
@@ -2944,82 +3101,12 @@
   EXPECT_THAT(error, HasSubstr("Invalid image format 'Rgb32f'"));
 }
 
-TEST_F(SpvParserHandleTest, ImageWrite_FloatDest_IntegralSrc_IsError) {
-  const auto assembly = Preamble() + R"(
-     OpEntryPoint Fragment %main "main"
-     OpExecutionMode %main OriginUpperLeft
-     OpName %coords12 "coords12"
-     OpDecorate %20 DescriptorSet 2
-     OpDecorate %20 Binding 1
-)" + CommonBasicTypes() +
-                        R"(
-     %im_ty = OpTypeImage %void 2D 0 0 0 2 R32f
-     %ptr_im_ty = OpTypePointer UniformConstant %im_ty
-
-     %20 = OpVariable %ptr_im_ty UniformConstant
-
-     %main = OpFunction %void None %voidfn
-     %entry = OpLabel
-
-     %f1 = OpCopyObject %float %float_1
-
-     %coords12 = OpCopyObject %v2float %the_vf12
-
-     %im = OpLoad %im_ty %20
-     OpImageWrite %im %coords12 %uint_0
-     OpReturn
-     OpFunctionEnd
-  )";
-  auto p = parser(test::Assemble(assembly));
-  EXPECT_FALSE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(p->error(),
-              Eq("can only write float or float vector to a storage image with "
-                 "floating texel format: OpImageWrite %53 %2 %13"))
-      << p->error();
-}
-
-TEST_F(SpvParserHandleTest, ImageWrite_IntegralDest_FloatSrc_IsError) {
-  const auto assembly = Preamble() + R"(
-     OpEntryPoint Fragment %main "main"
-     OpExecutionMode %main OriginUpperLeft
-     OpName %coords12 "coords12"
-     OpDecorate %20 DescriptorSet 2
-     OpDecorate %20 Binding 1
-)" + CommonBasicTypes() +
-                        R"(
-     %im_ty = OpTypeImage %void 2D 0 0 0 2 R32ui
-     %ptr_im_ty = OpTypePointer UniformConstant %im_ty
-
-     %20 = OpVariable %ptr_im_ty UniformConstant
-
-     %main = OpFunction %void None %voidfn
-     %entry = OpLabel
-
-     %f1 = OpCopyObject %float %float_1
-
-     %coords12 = OpCopyObject %v2float %f1
-
-     %im = OpLoad %im_ty %20
-     OpImageWrite %im %coords12 %f1
-     OpReturn
-     OpFunctionEnd
-  )";
-  auto p = parser(test::Assemble(assembly));
-  EXPECT_FALSE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(p->error(),
-              Eq("float or float vector can only be written to a storage image "
-                 "with floating texel format: OpImageWrite %53 %2 %52"))
-      << p->error();
-}
-
 INSTANTIATE_TEST_SUITE_P(
-    // Convert texel values when the sampled type of the texture is of the
-    // wrong signedness:
-    //  unsigned int channel type -> signed int sampled texture
-    //  signed int channel type -> unsigned int sampled texture
-    // (It is already a SPIR-V validation rule that floating point texels
-    // must already be used with textures of floating point sampled types)
-    ImageWrite_ConvertTexelOperand_Signedness,
+    // The texel operand signedness must match the channel type signedness.
+    // SPIR-V validation checks that.
+    // This suite is for the cases where they are integral and the same
+    // signedness.
+    ImageWrite_ConvertTexelOperand_SameSignedness,
     SpvParserHandleTest_ImageAccessTest,
     ::testing::ValuesIn(std::vector<ImageAccessCase>{
         // Sampled type is unsigned int, texel is unsigned int
@@ -3042,50 +3129,6 @@
         Identifier[not set]{vu1234}
       )
     })"},
-        // Sampled type is unsigned int, texel is signed int
-        {"%uint 2D 0 0 0 2 Rgba32ui", "OpImageWrite %im %vi12 %vi1234",
-         R"(
-  Variable{
-    Decorations{
-      GroupDecoration{2}
-      BindingDecoration{1}
-    }
-    x_20
-    uniform_constant
-    __access_control_write_only__storage_texture_2d_rgba32uint
-  })",
-         R"(Call[not set]{
-      Identifier[not set]{textureStore}
-      (
-        Identifier[not set]{x_20}
-        Identifier[not set]{vi12}
-        Bitcast[not set]<__vec_4__u32>{
-          Identifier[not set]{vi1234}
-        }
-      )
-    })"},
-        // Sampled type is signed int, texel is unsigned int
-        {"%int 2D 0 0 0 2 Rgba32i", "OpImageWrite %im %vi12 %vu1234",
-         R"(
-  Variable{
-    Decorations{
-      GroupDecoration{2}
-      BindingDecoration{1}
-    }
-    x_20
-    uniform_constant
-    __access_control_write_only__storage_texture_2d_rgba32sint
-  })",
-         R"(Call[not set]{
-      Identifier[not set]{textureStore}
-      (
-        Identifier[not set]{x_20}
-        Identifier[not set]{vi12}
-        Bitcast[not set]<__vec_4__i32>{
-          Identifier[not set]{vu1234}
-        }
-      )
-    })"},
         // Sampled type is signed int, texel is signed int
         {"%int 2D 0 0 0 2 Rgba32i", "OpImageWrite %im %vi12 %vi1234",
          R"(
@@ -3107,6 +3150,167 @@
       )
     })"}}));
 
+INSTANTIATE_TEST_SUITE_P(
+    // Error out when OpImageWrite write texel differ in float vs. integral
+    ImageWrite_ConvertTexelOperand_DifferentFloatishness_IsError,
+    // Use the ImageDeclTest so we can check the error.
+    SpvParserHandleTest_ImageDeclTest,
+    ::testing::ValuesIn(std::vector<ImageDeclCase>{
+        // Sampled type is float, texel is signed int
+        {"%uint 2D 0 0 0 2 Rgba32f", "OpImageWrite %im %vi12 %vi1234",
+         "invalid texel type for storage texture write: component must be "
+         "float, signed integer, or unsigned integer to match the texture "
+         "channel type: OpImageWrite",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_rgba32float
+  })"},
+        // Sampled type is float, texel is unsigned int
+        {"%int 2D 0 0 0 2 Rgba32f", "OpImageWrite %im %vi12 %vu1234",
+         "invalid texel type for storage texture write: component must be "
+         "float, signed integer, or unsigned integer to match the texture "
+         "channel type: OpImageWrite",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_rgba32float
+  })"},
+        // Sampled type is unsigned int, texel is float
+        {"%uint 2D 0 0 0 2 Rgba32ui", "OpImageWrite %im %vi12 %vf1234",
+         "invalid texel type for storage texture write: component must be "
+         "float, signed integer, or unsigned integer to match the texture "
+         "channel type: OpImageWrite",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_rgba32uint
+  })"},
+        // Sampled type is signed int, texel is float
+        {"%int 2D 0 0 0 2 Rgba32i", "OpImageWrite %im %vi12 %vf1234",
+         "invalid texel type for storage texture write: component must be "
+         "float, signed integer, or unsigned integer to match the texture "
+         "channel type: OpImageWrite",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_rgba32sint
+  })"}}));
+
+INSTANTIATE_TEST_SUITE_P(
+    // Error out when OpImageWrite write texel signedness is different.
+    ImageWrite_ConvertTexelOperand_DifferentSignedness_IsError,
+    // Use the ImageDeclTest so we can check the error.
+    SpvParserHandleTest_ImageDeclTest,
+    ::testing::ValuesIn(std::vector<ImageDeclCase>{
+        // Sampled type is unsigned int, texel is signed int
+        {"%uint 2D 0 0 0 2 Rgba32ui", "OpImageWrite %im %vi12 %vi1234",
+         "invalid texel type for storage texture write: component must be "
+         "float, signed integer, or unsigned integer to match the texture "
+         "channel type: OpImageWrite",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_rgba32uint
+  })"},
+        // Sampled type is signed int, texel is unsigned int
+        {"%int 2D 0 0 0 2 Rgba32i", "OpImageWrite %im %vi12 %vu1234",
+         "invalid texel type for storage texture write: component must be "
+         "float, signed integer, or unsigned integer to match the texture "
+         "channel type: OpImageWrite",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_rgba32sint
+  })"}}));
+
+INSTANTIATE_TEST_SUITE_P(
+    // Show that zeros of the correct integer signedness are
+    // created when expanding an integer vector.
+    ImageWrite_ConvertTexelOperand_Signedness_AndWidening,
+    SpvParserHandleTest_ImageAccessTest,
+    ::testing::ValuesIn(std::vector<ImageAccessCase>{
+        // Source unsigned, dest unsigned
+        {"%uint 2D 0 0 0 2 R32ui", "OpImageWrite %im %vi12 %vu12",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_r32uint
+  })",
+         R"(Call[not set]{
+      Identifier[not set]{textureStore}
+      (
+        Identifier[not set]{x_20}
+        Identifier[not set]{vi12}
+        TypeConstructor[not set]{
+          __vec_4__u32
+          Identifier[not set]{vu12}
+          ScalarConstructor[not set]{0u}
+          ScalarConstructor[not set]{0u}
+        }
+      )
+    })"},
+        // Source signed, dest signed
+        {"%int 2D 0 0 0 2 R32i", "OpImageWrite %im %vi12 %vi12",
+         R"(
+  Variable{
+    Decorations{
+      GroupDecoration{2}
+      BindingDecoration{1}
+    }
+    x_20
+    uniform_constant
+    __access_control_write_only__storage_texture_2d_r32sint
+  })",
+         R"(Call[not set]{
+      Identifier[not set]{textureStore}
+      (
+        Identifier[not set]{x_20}
+        Identifier[not set]{vi12}
+        TypeConstructor[not set]{
+          __vec_4__i32
+          Identifier[not set]{vi12}
+          ScalarConstructor[not set]{0}
+          ScalarConstructor[not set]{0}
+        }
+      )
+    })"}}));
+
 INSTANTIATE_TEST_SUITE_P(ImageRead_OptionalParams,
                          SpvParserHandleTest_ImageAccessTest,
                          ::testing::ValuesIn(std::vector<ImageAccessCase>{
@@ -4637,150 +4841,6 @@
         // Not in WebGPU
     }));
 
-struct ImageDeclCase {
-  // SPIR-V image type, excluding result ID and opcode
-  std::string spirv_image_type_details;
-  std::string spirv_image_access;  // Optional instruction to provoke use
-  std::string expected_error;
-  std::string expected_decl;
-};
-
-inline std::ostream& operator<<(std::ostream& out, const ImageDeclCase& c) {
-  out << "ImageDeclCase(" << c.spirv_image_type_details << "\n"
-      << "access: " << c.spirv_image_access << "\n"
-      << "error: " << c.expected_error << "\n"
-      << "decl:" << c.expected_decl << "\n)";
-  return out;
-}
-
-using SpvParserHandleTest_ImageDeclTest =
-    SpvParserTestBase<::testing::TestWithParam<ImageDeclCase>>;
-
-TEST_P(SpvParserHandleTest_ImageDeclTest, DeclareHandle) {
-  // Only declare the sampled image type, and the associated variable
-  // if the requested image type is a sampled image type and not multisampled.
-  const bool is_sampled_image_type = GetParam().spirv_image_type_details.find(
-                                         "0 1 Unknown") != std::string::npos;
-  const auto assembly =
-      Preamble() + R"(
-     OpEntryPoint Fragment %100 "main"
-     OpExecutionMode %100 OriginUpperLeft
-     OpName %float_var "float_var"
-     OpName %ptr_float "ptr_float"
-     OpName %i1 "i1"
-     OpName %vi12 "vi12"
-     OpName %vi123 "vi123"
-     OpName %vi1234 "vi1234"
-     OpName %u1 "u1"
-     OpName %vu12 "vu12"
-     OpName %vu123 "vu123"
-     OpName %vu1234 "vu1234"
-     OpName %f1 "f1"
-     OpName %vf12 "vf12"
-     OpName %vf123 "vf123"
-     OpName %vf1234 "vf1234"
-     OpDecorate %10 DescriptorSet 0
-     OpDecorate %10 Binding 0
-     OpDecorate %20 DescriptorSet 2
-     OpDecorate %20 Binding 1
-     OpDecorate %30 DescriptorSet 0
-     OpDecorate %30 Binding 1
-)" + CommonBasicTypes() +
-      R"(
-     %sampler = OpTypeSampler
-     %ptr_sampler = OpTypePointer UniformConstant %sampler
-     %im_ty = OpTypeImage )" +
-      GetParam().spirv_image_type_details + R"(
-     %ptr_im_ty = OpTypePointer UniformConstant %im_ty
-)" + (is_sampled_image_type ? " %si_ty = OpTypeSampledImage %im_ty " : "") +
-      R"(
-
-     %ptr_float = OpTypePointer Function %float
-
-     %10 = OpVariable %ptr_sampler UniformConstant
-     %20 = OpVariable %ptr_im_ty UniformConstant
-     %30 = OpVariable %ptr_sampler UniformConstant ; comparison sampler, when needed
-
-     %100 = OpFunction %void None %voidfn
-     %entry = OpLabel
-
-     %float_var = OpVariable %ptr_float Function
-
-     %i1 = OpCopyObject %int %int_1
-     %vi12 = OpCopyObject %v2int %the_vi12
-     %vi123 = OpCopyObject %v3int %the_vi123
-     %vi1234 = OpCopyObject %v4int %the_vi1234
-
-     %u1 = OpCopyObject %uint %uint_1
-     %vu12 = OpCopyObject %v2uint %the_vu12
-     %vu123 = OpCopyObject %v3uint %the_vu123
-     %vu1234 = OpCopyObject %v4uint %the_vu1234
-
-     %f1 = OpCopyObject %float %float_1
-     %vf12 = OpCopyObject %v2float %the_vf12
-     %vf123 = OpCopyObject %v3float %the_vf123
-     %vf1234 = OpCopyObject %v4float %the_vf1234
-
-     %sam = OpLoad %sampler %10
-     %im = OpLoad %im_ty %20
-
-)" +
-      (is_sampled_image_type
-           ? " %sampled_image = OpSampledImage %si_ty %im %sam "
-           : "") +
-      GetParam().spirv_image_access +
-      R"(
-     ; Use an anchor for the cases when the image access doesn't have a result ID.
-     %1000 = OpCopyObject %uint %uint_0
-
-     OpReturn
-     OpFunctionEnd
-  )";
-  auto p = parser(test::Assemble(assembly));
-  const bool succeeded = p->BuildAndParseInternalModule();
-  if (succeeded) {
-    EXPECT_TRUE(GetParam().expected_error.empty());
-    const auto got = p->program().to_str();
-    EXPECT_THAT(got, HasSubstr(GetParam().expected_decl));
-  } else {
-    EXPECT_FALSE(GetParam().expected_error.empty());
-    EXPECT_THAT(p->error(), HasSubstr(GetParam().expected_error));
-  }
-}
-
-INSTANTIATE_TEST_SUITE_P(
-    Multisampled_Only2DNonArrayedIsValid,
-    SpvParserHandleTest_ImageDeclTest,
-    ::testing::ValuesIn(std::vector<ImageDeclCase>{
-        {"%float 1D 0 0 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
-         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
-        {"%float 1D 0 1 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
-         "WGSL arrayed textures must be 2d_array or cube_array: ", ""},
-        {"%float 2D 0 0 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
-         "", R"(
-  Variable{
-    Decorations{
-      GroupDecoration{2}
-      BindingDecoration{1}
-    }
-    x_20
-    uniform_constant
-    __multisampled_texture_2d__f32
-  }
-)"},
-        {"%float 2D 0 1 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
-         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
-        {"%float 3D 0 0 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
-         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
-        {"%float 3D 0 1 1 1 Unknown", "%result = OpImageQuerySamples %uint %im",
-         "WGSL arrayed textures must be 2d_array or cube_array: ", ""},
-        {"%float Cube 0 0 1 1 Unknown",
-         "%result = OpImageQuerySamples %uint %im",
-         "WGSL multisampled textures must be 2d and non-arrayed: ", ""},
-        {"%float Cube 0 1 1 1 Unknown",
-         "%result = OpImageQuerySamples %uint %im",
-         "WGSL multisampled textures must be 2d and non-arrayed: ", ""}}));
-
 struct ImageCoordsCase {
   // SPIR-V image type, excluding result ID and opcode
   std::string spirv_image_type_details;
@@ -5372,7 +5432,7 @@
 }
 )"}},
         {"%float 1D 0 0 0 2 R32f",
-         "OpImageWrite %im %u1 %float_1",
+         "OpImageWrite %im %u1 %vf1234",
          "",
          {R"(TypeConstructor[not set]{
   __i32
@@ -5397,7 +5457,7 @@
 }
 )"}},
         {"%float 2D 0 0 0 2 R32f",
-         "OpImageWrite %im %vu12 %float_1",
+         "OpImageWrite %im %vu12 %vf1234",
          "",
          {R"(TypeConstructor[not set]{
   __vec_2__i32
@@ -5452,7 +5512,7 @@
 }
 )"}},
         {"%float 2D 0 1 0 2 R32f",
-         "OpImageWrite %im %vu123 %float_1",
+         "OpImageWrite %im %vu123 %vf1234",
          "",
          {
              R"(TypeConstructor[not set]{