[spirv-reader] Add transform to handle shader IO
Move module-scope output variables to the private address space, wrap
entry points that use them, and then copy their values to the return
value of the wrapper function.
Replace module-scope input variables with function parameters and pass
them down the call stack from the entry point.
Modify the type of sample_mask builtins to a scalar u32.
Bug: 42250952
Change-Id: I01c958803ec4f88c1f2dbb21fdf2206501145176
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/169223
Reviewed-by: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/tint/lang/spirv/reader/lower/BUILD.bazel b/src/tint/lang/spirv/reader/lower/BUILD.bazel
index 01fd572..1348c8d 100644
--- a/src/tint/lang/spirv/reader/lower/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/lower/BUILD.bazel
@@ -40,10 +40,12 @@
name = "lower",
srcs = [
"lower.cc",
+ "shader_io.cc",
"vector_element_pointer.cc",
],
hdrs = [
"lower.h",
+ "shader_io.h",
"vector_element_pointer.h",
],
deps = [
@@ -52,6 +54,7 @@
"//src/tint/lang/core/constant",
"//src/tint/lang/core/intrinsic",
"//src/tint/lang/core/ir",
+ "//src/tint/lang/core/ir/transform/common",
"//src/tint/lang/core/type",
"//src/tint/utils/containers",
"//src/tint/utils/diagnostic",
@@ -74,6 +77,7 @@
name = "test",
alwayslink = True,
srcs = [
+ "shader_io_test.cc",
"vector_element_pointer_test.cc",
],
deps = [
diff --git a/src/tint/lang/spirv/reader/lower/BUILD.cmake b/src/tint/lang/spirv/reader/lower/BUILD.cmake
index 5f0e866..98de660 100644
--- a/src/tint/lang/spirv/reader/lower/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/lower/BUILD.cmake
@@ -41,6 +41,8 @@
tint_add_target(tint_lang_spirv_reader_lower lib
lang/spirv/reader/lower/lower.cc
lang/spirv/reader/lower/lower.h
+ lang/spirv/reader/lower/shader_io.cc
+ lang/spirv/reader/lower/shader_io.h
lang/spirv/reader/lower/vector_element_pointer.cc
lang/spirv/reader/lower/vector_element_pointer.h
)
@@ -51,6 +53,7 @@
tint_lang_core_constant
tint_lang_core_intrinsic
tint_lang_core_ir
+ tint_lang_core_ir_transform_common
tint_lang_core_type
tint_utils_containers
tint_utils_diagnostic
@@ -72,6 +75,7 @@
# Kind: test
################################################################################
tint_add_target(tint_lang_spirv_reader_lower_test test
+ lang/spirv/reader/lower/shader_io_test.cc
lang/spirv/reader/lower/vector_element_pointer_test.cc
)
diff --git a/src/tint/lang/spirv/reader/lower/BUILD.gn b/src/tint/lang/spirv/reader/lower/BUILD.gn
index b5f0342..37c4709 100644
--- a/src/tint/lang/spirv/reader/lower/BUILD.gn
+++ b/src/tint/lang/spirv/reader/lower/BUILD.gn
@@ -46,6 +46,8 @@
sources = [
"lower.cc",
"lower.h",
+ "shader_io.cc",
+ "shader_io.h",
"vector_element_pointer.cc",
"vector_element_pointer.h",
]
@@ -55,6 +57,7 @@
"${tint_src_dir}/lang/core/constant",
"${tint_src_dir}/lang/core/intrinsic",
"${tint_src_dir}/lang/core/ir",
+ "${tint_src_dir}/lang/core/ir/transform/common",
"${tint_src_dir}/lang/core/type",
"${tint_src_dir}/utils/containers",
"${tint_src_dir}/utils/diagnostic",
@@ -73,7 +76,10 @@
}
if (tint_build_unittests) {
tint_unittests_source_set("unittests") {
- sources = [ "vector_element_pointer_test.cc" ]
+ sources = [
+ "shader_io_test.cc",
+ "vector_element_pointer_test.cc",
+ ]
deps = [
"${tint_src_dir}:gmock_and_gtest",
"${tint_src_dir}/api/common",
diff --git a/src/tint/lang/spirv/reader/lower/lower.cc b/src/tint/lang/spirv/reader/lower/lower.cc
index 25f546c..433d0cd 100644
--- a/src/tint/lang/spirv/reader/lower/lower.cc
+++ b/src/tint/lang/spirv/reader/lower/lower.cc
@@ -28,6 +28,7 @@
#include "src/tint/lang/spirv/reader/lower/lower.h"
#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/spirv/reader/lower/shader_io.h"
#include "src/tint/lang/spirv/reader/lower/vector_element_pointer.h"
namespace tint::spirv::reader {
@@ -42,6 +43,7 @@
} while (false)
RUN_TRANSFORM(lower::VectorElementPointer, mod);
+ RUN_TRANSFORM(lower::ShaderIO, mod);
if (auto res = core::ir::ValidateAndDumpIfNeeded(mod, "end of lowering from SPIR-V");
res != Success) {
diff --git a/src/tint/lang/spirv/reader/lower/shader_io.cc b/src/tint/lang/spirv/reader/lower/shader_io.cc
new file mode 100644
index 0000000..a786791
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/shader_io.cc
@@ -0,0 +1,426 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/lower/shader_io.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/transform/common/referenced_module_vars.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::spirv::reader::lower {
+
+namespace {
+
+using namespace tint::core::fluent_types; // NOLINT
+
+/// PIMPL state for the transform.
+struct State {
+ /// The IR module.
+ core::ir::Module& ir;
+
+ /// The IR builder.
+ core::ir::Builder b{ir};
+
+ /// The type manager.
+ core::type::Manager& ty{ir.Types()};
+
+ /// A map from block to its containing function.
+ Hashmap<core::ir::Block*, core::ir::Function*, 64> block_to_function{};
+
+ /// A map from each function to a map from input variable to parameter.
+ Hashmap<core::ir::Function*, Hashmap<core::ir::Var*, core::ir::Value*, 4>, 8>
+ function_parameter_map{};
+
+ /// The set of output variables that have been processed.
+ Hashset<core::ir::Var*, 4> output_variables{};
+
+ /// The mapping from functions to their transitively referenced output variables.
+ core::ir::ReferencedModuleVars referenced_output_vars{
+ ir, [](const core::ir::Var* var) {
+ auto* view = var->Result(0)->Type()->As<core::type::MemoryView>();
+ return view && view->AddressSpace() == core::AddressSpace::kOut;
+ }};
+
+ /// Process the module.
+ void Process() {
+ // Process outputs first, as that may introduce new functions that input variables need to
+ // be propagated through.
+ ProcessOutputs();
+ ProcessInputs();
+ }
+
+ /// Process output variables.
+ /// Changes output variables to the `private` address space and wraps entry points that produce
+ /// outputs with new functions that copy the outputs from the private variables to the return
+ /// value.
+ void ProcessOutputs() {
+ // Update entry point functions to return their outputs, using a wrapper function.
+ // Use a worklist as `ProcessEntryPointOutputs()` will add new functions.
+ Vector<core::ir::Function*, 4> entry_points;
+ for (auto& func : ir.functions) {
+ if (func->Stage() != core::ir::Function::PipelineStage::kUndefined) {
+ entry_points.Push(func);
+ }
+ }
+ for (auto& ep : entry_points) {
+ ProcessEntryPointOutputs(ep);
+ }
+
+ // Remove attributes from all of the original structs and module-scope output variables.
+ // This is done last as we need to copy attributes during `ProcessEntryPointOutputs()`.
+ for (auto& var : output_variables) {
+ var->SetAttributes({});
+ if (auto* str = var->Result(0)->Type()->UnwrapPtr()->As<core::type::Struct>()) {
+ for (auto* member : str->Members()) {
+ // TODO(crbug.com/tint/745): Remove the const_cast.
+ const_cast<core::type::StructMember*>(member)->SetAttributes({});
+ }
+ }
+ }
+ }
+
+ /// Process input variables.
+ /// Pass inputs down the call stack as parameters to any functions that need them.
+ void ProcessInputs() {
+ // Seed the block-to-function map with the function entry blocks.
+ for (auto& func : ir.functions) {
+ block_to_function.Add(func->Block(), func);
+ }
+
+ // Gather the list of all module-scope input variables.
+ Vector<core::ir::Var*, 4> inputs;
+ for (auto* global : *ir.root_block) {
+ if (auto* var = global->As<core::ir::Var>()) {
+ auto addrspace = var->Result(0)->Type()->As<core::type::Pointer>()->AddressSpace();
+ if (addrspace == core::AddressSpace::kIn) {
+ inputs.Push(var);
+ }
+ }
+ }
+
+ // Replace the input variables with function parameters.
+ for (auto* var : inputs) {
+ ReplaceInputPointerUses(var, var->Result(0));
+ var->Destroy();
+ }
+ }
+
+ /// Replace an output pointer address space to make it `private`.
+ /// @param value the output variable
+ void ReplaceOutputPointerAddressSpace(core::ir::InstructionResult* value) {
+ // Change the address space to `private`.
+ auto* old_ptr_type = value->Type();
+ auto* new_ptr_type = ty.ptr(core::AddressSpace::kPrivate, old_ptr_type->UnwrapPtr());
+ value->SetType(new_ptr_type);
+
+ // Update all uses of the module-scope variable.
+ value->ForEachUse([&](core::ir::Usage use) {
+ if (auto* access = use.instruction->As<core::ir::Access>()) {
+ ReplaceOutputPointerAddressSpace(access->Result(0));
+ } else if (!use.instruction->IsAnyOf<core::ir::Load, core::ir::LoadVectorElement,
+ core::ir::Store, core::ir::StoreVectorElement>()) {
+ TINT_UNREACHABLE()
+ << "unexpected instruction: " << use.instruction->TypeInfo().name;
+ }
+ });
+ }
+
+ /// Process the outputs of an entry point function, adding a wrapper function to forward outputs
+ /// through the return value.
+ /// @param ep the entry point
+ void ProcessEntryPointOutputs(core::ir::Function* ep) {
+ const auto& referenced_outputs = referenced_output_vars.TransitiveReferences(ep);
+ if (referenced_outputs.IsEmpty()) {
+ return;
+ }
+
+ // Add a wrapper function to return either a single value or a struct.
+ auto* wrapper = b.Function(ty.void_(), ep->Stage());
+ if (auto name = ir.NameOf(ep)) {
+ ir.SetName(ep, name.Name() + "_inner");
+ ir.SetName(wrapper, name);
+ }
+
+ // Call the original entry point and make it a regular function.
+ ep->SetStage(core::ir::Function::PipelineStage::kUndefined);
+ b.Append(wrapper->Block(), [&] { //
+ b.Call(ep);
+ });
+
+ // Collect all outputs into a list of struct member declarations.
+ // Also add instructions to load their final values in the wrapper function.
+ Vector<core::ir::Value*, 4> results;
+ Vector<core::type::Manager::StructMemberDesc, 4> output_descriptors;
+ auto add_output = [&](Symbol name, const core::type::Type* type,
+ core::type::StructMemberAttributes attributes) {
+ if (!name) {
+ name = ir.symbols.New();
+ }
+ output_descriptors.Push(core::type::Manager::StructMemberDesc{name, type, attributes});
+ };
+ for (auto* var : referenced_outputs) {
+ // Change the address space of the variable to private and update its uses, if we
+ // haven't already seen this variable.
+ if (output_variables.Add(var)) {
+ ReplaceOutputPointerAddressSpace(var->Result(0));
+ }
+
+ // Copy the variable attributes to the struct member.
+ const auto& original_attributes = var->Attributes();
+ core::type::StructMemberAttributes var_attributes;
+ var_attributes.invariant = original_attributes.invariant;
+ var_attributes.builtin = original_attributes.builtin;
+ var_attributes.location = original_attributes.location;
+ var_attributes.interpolation = original_attributes.interpolation;
+
+ auto var_type = var->Result(0)->Type()->UnwrapPtr();
+ if (auto* str = var_type->As<core::type::Struct>()) {
+ // Add an output for each member of the struct.
+ for (auto* member : str->Members()) {
+ // Use the base variable attributes if not specified directly on the member.
+ auto member_attributes = member->Attributes();
+ if (auto base_loc = var_attributes.location) {
+ // Location values increment from the base location value on the variable.
+ member_attributes.location = base_loc.value() + member->Index();
+ }
+ if (!member_attributes.interpolation) {
+ member_attributes.interpolation = var_attributes.interpolation;
+ }
+
+ add_output(member->Name(), member->Type(), std::move(member_attributes));
+
+ // Load the final result from the member of the original struct variable.
+ b.Append(wrapper->Block(), [&] { //
+ auto* access =
+ b.Access(ty.ptr<private_>(member->Type()), var, u32(member->Index()));
+ results.Push(b.Load(access)->Result(0));
+ });
+ }
+ } else {
+ // Load the final result from the original variable.
+ b.Append(wrapper->Block(), [&] {
+ results.Push(b.Load(var)->Result(0));
+
+ // If we're dealing with sample_mask, extract the scalar from the array.
+ if (var_attributes.builtin == core::BuiltinValue::kSampleMask) {
+ var_type = ty.u32();
+ results.Back() = b.Access(ty.u32(), results.Back(), u32(0))->Result(0);
+ }
+ });
+ add_output(ir.NameOf(var), var_type, std::move(var_attributes));
+ }
+ }
+
+ if (output_descriptors.Length() == 1) {
+ // Copy the output attributes to the function return.
+ const auto& attributes = output_descriptors[0].attributes;
+ wrapper->SetReturnInvariant(attributes.invariant);
+ if (attributes.builtin) {
+ wrapper->SetReturnBuiltin(attributes.builtin.value());
+ } else if (attributes.location) {
+ core::ir::Location loc;
+ loc.value = attributes.location.value();
+ loc.interpolation = attributes.interpolation;
+ wrapper->SetReturnLocation(std::move(loc));
+ }
+
+ // Return the output from the wrapper function.
+ wrapper->SetReturnType(output_descriptors[0].type);
+ b.Append(wrapper->Block(), [&] { //
+ b.Return(wrapper, results[0]);
+ });
+ } else {
+ // Create a struct to hold all of the output values.
+ auto* str = ty.Struct(ir.symbols.New(), std::move(output_descriptors));
+ wrapper->SetReturnType(str);
+
+ // Collect the output values and return them from the wrapper function.
+ b.Append(wrapper->Block(), [&] { //
+ b.Return(wrapper, b.Construct(str, std::move(results)));
+ });
+ }
+ }
+
+ /// Replace a use of an input pointer value.
+ /// @param var the originating input variable
+ /// @param value the input pointer value
+ void ReplaceInputPointerUses(core::ir::Var* var, core::ir::Value* value) {
+ Vector<core::ir::Instruction*, 8> to_destroy;
+ value->ForEachUse([&](core::ir::Usage use) {
+ auto* object = value;
+ if (object->Type()->Is<core::type::Pointer>()) {
+ // Get (or create) the function parameter that will replace the variable.
+ auto* func = ContainingFunction(use.instruction);
+ object = GetParameter(func, var);
+ }
+
+ Switch(
+ use.instruction,
+ [&](core::ir::Load* l) {
+ // Fold the load away and replace its uses with the new parameter.
+ l->Result(0)->ReplaceAllUsesWith(object);
+ to_destroy.Push(l);
+ },
+ [&](core::ir::LoadVectorElement* lve) {
+ // Replace the vector element load with an access instruction.
+ auto* access = b.AccessWithResult(lve->DetachResult(), object, lve->Index());
+ access->InsertBefore(lve);
+ to_destroy.Push(lve);
+ },
+ [&](core::ir::Access* a) {
+ if (!a->Indices().IsEmpty()) {
+ // Remove the pointer from the source and destination type.
+ a->SetOperand(core::ir::Access::kObjectOperandOffset, object);
+ a->Result(0)->SetType(a->Result(0)->Type()->UnwrapPtr());
+ ReplaceInputPointerUses(var, a->Result(0));
+ } else {
+ // Fold the access away and replace its uses.
+ ReplaceInputPointerUses(var, a->Result(0));
+ to_destroy.Push(a);
+ }
+ },
+ TINT_ICE_ON_NO_MATCH);
+ });
+
+ // Clean up orphaned instructions.
+ for (auto* inst : to_destroy) {
+ inst->Destroy();
+ }
+ }
+
+ /// Get the function that contains an instruction.
+ /// @param inst the instruction
+ /// @returns the function
+ core::ir::Function* ContainingFunction(core::ir::Instruction* inst) {
+ return block_to_function.GetOrAdd(inst->Block(), [&] { //
+ return ContainingFunction(inst->Block()->Parent());
+ });
+ }
+
+ /// Get or create a function parameter to replace a module-scope variable.
+ /// @param func the function
+ /// @param var the module-scope variable
+ /// @returns the function parameter
+ core::ir::Value* GetParameter(core::ir::Function* func, core::ir::Var* var) {
+ return function_parameter_map.GetOrAddZero(func).GetOrAdd(var, [&] {
+ const bool entry_point = func->Stage() != core::ir::Function::PipelineStage::kUndefined;
+ auto* var_type = var->Result(0)->Type()->UnwrapPtr();
+
+ // Use a scalar u32 for sample_mask builtins for entry point parameters.
+ if (entry_point && var->Attributes().builtin == core::BuiltinValue::kSampleMask) {
+ TINT_ASSERT(var_type->Is<core::type::Array>());
+ TINT_ASSERT(var_type->As<core::type::Array>()->ConstantCount() == 1u);
+ var_type = ty.u32();
+ }
+
+ // Create a new function parameter for the input.
+ auto* param = b.FunctionParam(var_type);
+ func->AppendParam(param);
+ if (auto name = ir.NameOf(var)) {
+ ir.SetName(param, name);
+ }
+
+ // Add attributes to the parameter if this is an entry point function.
+ if (entry_point) {
+ AddEntryPointParameterAttributes(param, var->Attributes());
+ }
+
+ // Update the callsites of this function.
+ func->ForEachUse([&](core::ir::Usage use) {
+ if (auto* call = use.instruction->As<core::ir::UserCall>()) {
+ // Recurse into the calling function.
+ auto* caller = ContainingFunction(call);
+ call->AppendArg(GetParameter(caller, var));
+ } else if (!use.instruction->Is<core::ir::Return>()) {
+ TINT_UNREACHABLE()
+ << "unexpected instruction: " << use.instruction->TypeInfo().name;
+ }
+ });
+
+ core::ir::Value* result = param;
+ if (entry_point && var->Attributes().builtin == core::BuiltinValue::kSampleMask) {
+ // Construct an array from the scalar sample_mask builtin value for entry points.
+ b.Prepend(func->Block(), [&] { //
+ result = b.Construct(var->Result(0)->Type()->UnwrapPtr(), param)->Result(0);
+ });
+ }
+ return result;
+ });
+ }
+
+ /// Add attributes to an entry point function parameter.
+ /// @param param the parameter
+ /// @param attributes the attributes
+ void AddEntryPointParameterAttributes(core::ir::FunctionParam* param,
+ const core::ir::IOAttributes& attributes) {
+ if (auto* str = param->Type()->UnwrapPtr()->As<core::type::Struct>()) {
+ for (auto* member : str->Members()) {
+ // Use the base variable attributes if not specified directly on the member.
+ auto member_attributes = member->Attributes();
+ if (auto base_loc = attributes.location) {
+ // Location values increment from the base location value on the variable.
+ member_attributes.location = base_loc.value() + member->Index();
+ }
+ if (!member_attributes.interpolation) {
+ member_attributes.interpolation = attributes.interpolation;
+ }
+ // TODO(crbug.com/tint/745): Remove the const_cast.
+ const_cast<core::type::StructMember*>(member)->SetAttributes(
+ std::move(member_attributes));
+ }
+ } else {
+ // Set attributes directly on the function parameter.
+ param->SetInvariant(attributes.invariant);
+ if (attributes.builtin) {
+ param->SetBuiltin(attributes.builtin.value());
+ } else if (attributes.location) {
+ core::ir::Location loc;
+ loc.value = attributes.location.value();
+ loc.interpolation = attributes.interpolation;
+ param->SetLocation(std::move(loc));
+ }
+ }
+ }
+};
+
+} // namespace
+
+Result<SuccessType> ShaderIO(core::ir::Module& ir) {
+ auto result = ValidateAndDumpIfNeeded(ir, "ShaderIO transform");
+ if (result != Success) {
+ return result.Failure();
+ }
+
+ State{ir}.Process();
+
+ return Success;
+}
+
+} // namespace tint::spirv::reader::lower
diff --git a/src/tint/lang/spirv/reader/lower/shader_io.h b/src/tint/lang/spirv/reader/lower/shader_io.h
new file mode 100644
index 0000000..8347585
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/shader_io.h
@@ -0,0 +1,48 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_READER_LOWER_SHADER_IO_H_
+#define SRC_TINT_LANG_SPIRV_READER_LOWER_SHADER_IO_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::spirv::reader::lower {
+
+/// ShaderIO is a transform that converts SPIR-V's style of shader IO (using global variables) into
+/// the form expected by Tint's core IR (using function parameters and return values).
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> ShaderIO(core::ir::Module& module);
+
+} // namespace tint::spirv::reader::lower
+
+#endif // SRC_TINT_LANG_SPIRV_READER_LOWER_SHADER_IO_H_
diff --git a/src/tint/lang/spirv/reader/lower/shader_io_test.cc b/src/tint/lang/spirv/reader/lower/shader_io_test.cc
new file mode 100644
index 0000000..d004762
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/shader_io_test.cc
@@ -0,0 +1,2267 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/lower/shader_io.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::spirv::reader::lower {
+namespace {
+
+using namespace tint::core::fluent_types; // NOLINT
+using namespace tint::core::number_suffixes; // NOLINT
+
+class SpirvReader_ShaderIOTest : public core::ir::transform::TransformTest {
+ protected:
+ core::type::StructMemberAttributes BuiltinAttrs(core::BuiltinValue builtin) {
+ core::type::StructMemberAttributes attrs;
+ attrs.builtin = builtin;
+ return attrs;
+ }
+ core::type::StructMemberAttributes LocationAttrs(
+ uint32_t location,
+ std::optional<core::Interpolation> interpolation = std::nullopt) {
+ core::type::StructMemberAttributes attrs;
+ attrs.location = location;
+ attrs.interpolation = interpolation;
+ return attrs;
+ }
+};
+
+TEST_F(SpirvReader_ShaderIOTest, NoInputsOrOutputs) {
+ auto* ep = b.Function("foo", ty.void_());
+ ep->SetStage(core::ir::Function::PipelineStage::kCompute);
+
+ b.Append(ep->Block(), [&] { //
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+%foo = @compute func():void {
+ $B1: {
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = src;
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs) {
+ auto* front_facing = b.Var("front_facing", ty.ptr(core::AddressSpace::kIn, ty.bool_()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kFrontFacing;
+ front_facing->SetAttributes(std::move(attributes));
+ }
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ attributes.invariant = true;
+ position->SetAttributes(std::move(attributes));
+ }
+ auto* color1 = b.Var("color1", ty.ptr(core::AddressSpace::kIn, ty.f32()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 0;
+ color1->SetAttributes(std::move(attributes));
+ }
+ auto* color2 = b.Var("color2", ty.ptr(core::AddressSpace::kIn, ty.f32()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kLinear,
+ core::InterpolationSampling::kSample};
+ color2->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(front_facing);
+ mod.root_block->Append(position);
+ mod.root_block->Append(color1);
+ mod.root_block->Append(color2);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ auto* ifelse = b.If(b.Load(front_facing));
+ b.Append(ifelse->True(), [&] {
+ auto* position_value = b.Load(position);
+ auto* color1_value = b.Load(color1);
+ auto* color2_value = b.Load(color2);
+ b.Multiply(ty.vec4<f32>(), position_value, b.Add(ty.f32(), color1_value, color2_value));
+ b.ExitIf(ifelse);
+ });
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %front_facing:ptr<__in, bool, read> = var @builtin(front_facing)
+ %position:ptr<__in, vec4<f32>, read> = var @invariant @builtin(position)
+ %color1:ptr<__in, f32, read> = var @location(0)
+ %color2:ptr<__in, f32, read> = var @location(1) @interpolate(linear, sample)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ %6:bool = load %front_facing
+ if %6 [t: $B3] { # if_1
+ $B3: { # true
+ %7:vec4<f32> = load %position
+ %8:f32 = load %color1
+ %9:f32 = load %color2
+ %10:f32 = add %8, %9
+ %11:vec4<f32> = mul %7, %10
+ exit_if # if_1
+ }
+ }
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%foo = @fragment func(%front_facing:bool [@front_facing], %position:vec4<f32> [@invariant, @position], %color1:f32 [@location(0)], %color2:f32 [@location(1), @interpolate(linear, sample)]):void {
+ $B1: {
+ if %front_facing [t: $B2] { # if_1
+ $B2: { # true
+ %6:f32 = add %color1, %color2
+ %7:vec4<f32> = mul %position, %6
+ exit_if # if_1
+ }
+ }
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_UsedByHelper) {
+ auto* front_facing = b.Var("front_facing", ty.ptr(core::AddressSpace::kIn, ty.bool_()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kFrontFacing;
+ front_facing->SetAttributes(std::move(attributes));
+ }
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ attributes.invariant = true;
+ position->SetAttributes(std::move(attributes));
+ }
+ auto* color1 = b.Var("color1", ty.ptr(core::AddressSpace::kIn, ty.f32()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 0;
+ color1->SetAttributes(std::move(attributes));
+ }
+ auto* color2 = b.Var("color2", ty.ptr(core::AddressSpace::kIn, ty.f32()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kLinear,
+ core::InterpolationSampling::kSample};
+ color2->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(front_facing);
+ mod.root_block->Append(position);
+ mod.root_block->Append(color1);
+ mod.root_block->Append(color2);
+
+ // Inner function has an existing parameter.
+ auto* param = b.FunctionParam("existing_param", ty.f32());
+ auto* foo = b.Function("foo", ty.void_());
+ foo->SetParams({param});
+ b.Append(foo->Block(), [&] {
+ auto* ifelse = b.If(b.Load(front_facing));
+ b.Append(ifelse->True(), [&] {
+ auto* position_value = b.Load(position);
+ auto* color1_value = b.Load(color1);
+ auto* color2_value = b.Load(color2);
+ auto* add = b.Add(ty.f32(), color1_value, color2_value);
+ auto* mul = b.Multiply(ty.vec4<f32>(), position_value, add);
+ b.Divide(ty.vec4<f32>(), mul, param);
+ b.ExitIf(ifelse);
+ });
+ b.Return(foo);
+ });
+
+ // Intermediate function has no existing parameters.
+ auto* bar = b.Function("bar", ty.void_());
+ b.Append(bar->Block(), [&] {
+ b.Call(foo, 42_f);
+ b.Return(bar);
+ });
+
+ auto* ep = b.Function("main", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ b.Call(bar);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %front_facing:ptr<__in, bool, read> = var @builtin(front_facing)
+ %position:ptr<__in, vec4<f32>, read> = var @invariant @builtin(position)
+ %color1:ptr<__in, f32, read> = var @location(0)
+ %color2:ptr<__in, f32, read> = var @location(1) @interpolate(linear, sample)
+}
+
+%foo = func(%existing_param:f32):void {
+ $B2: {
+ %7:bool = load %front_facing
+ if %7 [t: $B3] { # if_1
+ $B3: { # true
+ %8:vec4<f32> = load %position
+ %9:f32 = load %color1
+ %10:f32 = load %color2
+ %11:f32 = add %9, %10
+ %12:vec4<f32> = mul %8, %11
+ %13:vec4<f32> = div %12, %existing_param
+ exit_if # if_1
+ }
+ }
+ ret
+ }
+}
+%bar = func():void {
+ $B4: {
+ %15:void = call %foo, 42.0f
+ ret
+ }
+}
+%main = @fragment func():void {
+ $B5: {
+ %17:void = call %bar
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%foo = func(%existing_param:f32, %front_facing:bool, %position:vec4<f32>, %color1:f32, %color2:f32):void {
+ $B1: {
+ if %front_facing [t: $B2] { # if_1
+ $B2: { # true
+ %7:f32 = add %color1, %color2
+ %8:vec4<f32> = mul %position, %7
+ %9:vec4<f32> = div %8, %existing_param
+ exit_if # if_1
+ }
+ }
+ ret
+ }
+}
+%bar = func(%front_facing_1:bool, %position_1:vec4<f32>, %color1_1:f32, %color2_1:f32):void { # %front_facing_1: 'front_facing', %position_1: 'position', %color1_1: 'color1', %color2_1: 'color2'
+ $B3: {
+ %15:void = call %foo, 42.0f, %front_facing_1, %position_1, %color1_1, %color2_1
+ ret
+ }
+}
+%main = @fragment func(%front_facing_2:bool [@front_facing], %position_2:vec4<f32> [@invariant, @position], %color1_2:f32 [@location(0)], %color2_2:f32 [@location(1), @interpolate(linear, sample)]):void { # %front_facing_2: 'front_facing', %position_2: 'position', %color1_2: 'color1', %color2_2: 'color2'
+ $B4: {
+ %21:void = call %bar, %front_facing_2, %position_2, %color1_2, %color2_2
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_UsedEntryPointAndHelper) {
+ auto* gid = b.Var("gid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kGlobalInvocationId;
+ gid->SetAttributes(std::move(attributes));
+ }
+ auto* lid = b.Var("lid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kLocalInvocationId;
+ lid->SetAttributes(std::move(attributes));
+ }
+ auto* group_id = b.Var("group_id", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kWorkgroupId;
+ group_id->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(gid);
+ mod.root_block->Append(lid);
+ mod.root_block->Append(group_id);
+
+ // Use a subset of the inputs in the helper.
+ auto* foo = b.Function("foo", ty.void_());
+ b.Append(foo->Block(), [&] {
+ auto* gid_value = b.Load(gid);
+ auto* lid_value = b.Load(lid);
+ b.Add(ty.vec3<u32>(), gid_value, lid_value);
+ b.Return(foo);
+ });
+
+ // Use a different subset of the inputs in the entry point.
+ auto* ep = b.Function("main1", ty.void_(), core::ir::Function::PipelineStage::kCompute);
+ b.Append(ep->Block(), [&] {
+ auto* group_value = b.Load(group_id);
+ auto* gid_value = b.Load(gid);
+ b.Add(ty.vec3<u32>(), group_value, gid_value);
+ b.Call(foo);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %gid:ptr<__in, vec3<u32>, read> = var @builtin(global_invocation_id)
+ %lid:ptr<__in, vec3<u32>, read> = var @builtin(local_invocation_id)
+ %group_id:ptr<__in, vec3<u32>, read> = var @builtin(workgroup_id)
+}
+
+%foo = func():void {
+ $B2: {
+ %5:vec3<u32> = load %gid
+ %6:vec3<u32> = load %lid
+ %7:vec3<u32> = add %5, %6
+ ret
+ }
+}
+%main1 = @compute func():void {
+ $B3: {
+ %9:vec3<u32> = load %group_id
+ %10:vec3<u32> = load %gid
+ %11:vec3<u32> = add %9, %10
+ %12:void = call %foo
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%foo = func(%gid:vec3<u32>, %lid:vec3<u32>):void {
+ $B1: {
+ %4:vec3<u32> = add %gid, %lid
+ ret
+ }
+}
+%main1 = @compute func(%gid_1:vec3<u32> [@global_invocation_id], %lid_1:vec3<u32> [@local_invocation_id], %group_id:vec3<u32> [@workgroup_id]):void { # %gid_1: 'gid', %lid_1: 'lid'
+ $B2: {
+ %9:vec3<u32> = add %group_id, %gid_1
+ %10:void = call %foo, %gid_1, %lid_1
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_UsedEntryPointAndHelper_ForwardReference) {
+ auto* gid = b.Var("gid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kGlobalInvocationId;
+ gid->SetAttributes(std::move(attributes));
+ }
+ auto* lid = b.Var("lid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kLocalInvocationId;
+ lid->SetAttributes(std::move(attributes));
+ }
+ auto* group_id = b.Var("group_id", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kWorkgroupId;
+ group_id->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(gid);
+ mod.root_block->Append(lid);
+ mod.root_block->Append(group_id);
+
+ auto* ep = b.Function("main1", ty.void_(), core::ir::Function::PipelineStage::kCompute);
+ auto* foo = b.Function("foo", ty.void_());
+
+ // Use a subset of the inputs in the entry point.
+ b.Append(ep->Block(), [&] {
+ auto* group_value = b.Load(group_id);
+ auto* gid_value = b.Load(gid);
+ b.Add(ty.vec3<u32>(), group_value, gid_value);
+ b.Call(foo);
+ b.Return(ep);
+ });
+
+ // Use a different subset of the variables in the helper.
+ b.Append(foo->Block(), [&] {
+ auto* gid_value = b.Load(gid);
+ auto* lid_value = b.Load(lid);
+ b.Add(ty.vec3<u32>(), gid_value, lid_value);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %gid:ptr<__in, vec3<u32>, read> = var @builtin(global_invocation_id)
+ %lid:ptr<__in, vec3<u32>, read> = var @builtin(local_invocation_id)
+ %group_id:ptr<__in, vec3<u32>, read> = var @builtin(workgroup_id)
+}
+
+%main1 = @compute func():void {
+ $B2: {
+ %5:vec3<u32> = load %group_id
+ %6:vec3<u32> = load %gid
+ %7:vec3<u32> = add %5, %6
+ %8:void = call %foo
+ ret
+ }
+}
+%foo = func():void {
+ $B3: {
+ %10:vec3<u32> = load %gid
+ %11:vec3<u32> = load %lid
+ %12:vec3<u32> = add %10, %11
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%main1 = @compute func(%gid:vec3<u32> [@global_invocation_id], %lid:vec3<u32> [@local_invocation_id], %group_id:vec3<u32> [@workgroup_id]):void {
+ $B1: {
+ %5:vec3<u32> = add %group_id, %gid
+ %6:void = call %foo, %gid, %lid
+ ret
+ }
+}
+%foo = func(%gid_1:vec3<u32>, %lid_1:vec3<u32>):void { # %gid_1: 'gid', %lid_1: 'lid'
+ $B2: {
+ %10:vec3<u32> = add %gid_1, %lid_1
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_UsedByMultipleEntryPoints) {
+ auto* gid = b.Var("gid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kGlobalInvocationId;
+ gid->SetAttributes(std::move(attributes));
+ }
+ auto* lid = b.Var("lid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kLocalInvocationId;
+ lid->SetAttributes(std::move(attributes));
+ }
+ auto* group_id = b.Var("group_id", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kWorkgroupId;
+ group_id->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(gid);
+ mod.root_block->Append(lid);
+ mod.root_block->Append(group_id);
+
+ // Use a subset of the inputs in the helper.
+ auto* foo = b.Function("foo", ty.void_());
+ b.Append(foo->Block(), [&] {
+ auto* gid_value = b.Load(gid);
+ auto* lid_value = b.Load(lid);
+ b.Add(ty.vec3<u32>(), gid_value, lid_value);
+ b.Return(foo);
+ });
+
+ // Call the helper without directly referencing any inputs.
+ auto* ep1 = b.Function("main1", ty.void_(), core::ir::Function::PipelineStage::kCompute);
+ b.Append(ep1->Block(), [&] {
+ b.Call(foo);
+ b.Return(ep1);
+ });
+
+ // Reference another input and then call the helper.
+ auto* ep2 = b.Function("main2", ty.void_(), core::ir::Function::PipelineStage::kCompute);
+ b.Append(ep2->Block(), [&] {
+ auto* group_value = b.Load(group_id);
+ b.Add(ty.vec3<u32>(), group_value, group_value);
+ b.Call(foo);
+ b.Return(ep1);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %gid:ptr<__in, vec3<u32>, read> = var @builtin(global_invocation_id)
+ %lid:ptr<__in, vec3<u32>, read> = var @builtin(local_invocation_id)
+ %group_id:ptr<__in, vec3<u32>, read> = var @builtin(workgroup_id)
+}
+
+%foo = func():void {
+ $B2: {
+ %5:vec3<u32> = load %gid
+ %6:vec3<u32> = load %lid
+ %7:vec3<u32> = add %5, %6
+ ret
+ }
+}
+%main1 = @compute func():void {
+ $B3: {
+ %9:void = call %foo
+ ret
+ }
+}
+%main2 = @compute func():void {
+ $B4: {
+ %11:vec3<u32> = load %group_id
+ %12:vec3<u32> = add %11, %11
+ %13:void = call %foo
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%foo = func(%gid:vec3<u32>, %lid:vec3<u32>):void {
+ $B1: {
+ %4:vec3<u32> = add %gid, %lid
+ ret
+ }
+}
+%main1 = @compute func(%gid_1:vec3<u32> [@global_invocation_id], %lid_1:vec3<u32> [@local_invocation_id]):void { # %gid_1: 'gid', %lid_1: 'lid'
+ $B2: {
+ %8:void = call %foo, %gid_1, %lid_1
+ ret
+ }
+}
+%main2 = @compute func(%gid_2:vec3<u32> [@global_invocation_id], %lid_2:vec3<u32> [@local_invocation_id], %group_id:vec3<u32> [@workgroup_id]):void { # %gid_2: 'gid', %lid_2: 'lid'
+ $B3: {
+ %13:vec3<u32> = add %group_id, %group_id
+ %14:void = call %foo, %gid_2, %lid_2
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Input_LoadVectorElement) {
+ auto* lid = b.Var("lid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kLocalInvocationId;
+ lid->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(lid);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kCompute);
+ b.Append(ep->Block(), [&] {
+ b.LoadVectorElement(lid, 2_u);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %lid:ptr<__in, vec3<u32>, read> = var @builtin(local_invocation_id)
+}
+
+%foo = @compute func():void {
+ $B2: {
+ %3:u32 = load_vector_element %lid, 2u
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%foo = @compute func(%lid:vec3<u32> [@local_invocation_id]):void {
+ $B1: {
+ %3:u32 = access %lid, 2u
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Input_AccessChains) {
+ auto* lid = b.Var("lid", ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kLocalInvocationId;
+ lid->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(lid);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kCompute);
+ b.Append(ep->Block(), [&] {
+ auto* access_1 = b.Access(ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()), lid);
+ auto* access_2 = b.Access(ty.ptr(core::AddressSpace::kIn, ty.vec3<u32>()), access_1);
+ auto* vec = b.Load(access_2);
+ auto* z = b.LoadVectorElement(access_2, 2_u);
+ b.Multiply<vec3<u32>>(vec, z);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %lid:ptr<__in, vec3<u32>, read> = var @builtin(local_invocation_id)
+}
+
+%foo = @compute func():void {
+ $B2: {
+ %3:ptr<__in, vec3<u32>, read> = access %lid
+ %4:ptr<__in, vec3<u32>, read> = access %3
+ %5:vec3<u32> = load %4
+ %6:u32 = load_vector_element %4, 2u
+ %7:vec3<u32> = mul %5, %6
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+%foo = @compute func(%lid:vec3<u32> [@local_invocation_id]):void {
+ $B1: {
+ %3:u32 = access %lid, 2u
+ %4:vec3<u32> = mul %lid, %3
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_Struct_LocationOnEachMember) {
+ auto* colors_str = ty.Struct(
+ mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ LocationAttrs(1),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ LocationAttrs(2u, core::Interpolation{core::InterpolationType::kLinear,
+ core::InterpolationSampling::kCentroid}),
+ },
+ });
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kIn, colors_str));
+ mod.root_block->Append(colors);
+
+ auto* foo = b.Function("foo", ty.void_());
+ b.Append(foo->Block(), [&] {
+ auto* ptr = ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>());
+ auto* color1_value = b.Load(b.Access(ptr, colors, 0_u));
+ auto* color2_z_value = b.LoadVectorElement(b.Access(ptr, colors, 1_u), 2_u);
+ b.Multiply(ty.vec4<f32>(), color1_value, color2_z_value);
+ b.Return(foo);
+ });
+
+ auto* ep = b.Function("main", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ b.Call(foo);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(linear, centroid)
+}
+
+$B1: { # root
+ %colors:ptr<__in, Colors, read> = var
+}
+
+%foo = func():void {
+ $B2: {
+ %3:ptr<__in, vec4<f32>, read> = access %colors, 0u
+ %4:vec4<f32> = load %3
+ %5:ptr<__in, vec4<f32>, read> = access %colors, 1u
+ %6:f32 = load_vector_element %5, 2u
+ %7:vec4<f32> = mul %4, %6
+ ret
+ }
+}
+%main = @fragment func():void {
+ $B3: {
+ %9:void = call %foo
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(linear, centroid)
+}
+
+%foo = func(%colors:Colors):void {
+ $B1: {
+ %3:vec4<f32> = access %colors, 0u
+ %4:vec4<f32> = access %colors, 1u
+ %5:f32 = access %4, 2u
+ %6:vec4<f32> = mul %3, %5
+ ret
+ }
+}
+%main = @fragment func(%colors_1:Colors):void { # %colors_1: 'colors'
+ $B2: {
+ %9:void = call %foo, %colors_1
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_Struct_LocationOnVariable) {
+ auto* colors_str =
+ ty.Struct(mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ core::type::StructMemberAttributes{
+ /* location */ std::nullopt,
+ /* index */ std::nullopt,
+ /* color */ std::nullopt,
+ /* builtin */ std::nullopt,
+ /* interpolation */
+ core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid},
+ /* invariant */ false,
+ },
+ },
+ });
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kIn, colors_str));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ colors->SetAttributes(attributes);
+ }
+ mod.root_block->Append(colors);
+
+ auto* foo = b.Function("foo", ty.void_());
+ b.Append(foo->Block(), [&] {
+ auto* ptr = ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>());
+ auto* color1_value = b.Load(b.Access(ptr, colors, 0_u));
+ auto* color2_z_value = b.LoadVectorElement(b.Access(ptr, colors, 1_u), 2_u);
+ b.Multiply(ty.vec4<f32>(), color1_value, color2_z_value);
+ b.Return(foo);
+ });
+
+ auto* ep = b.Function("main", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ b.Call(foo);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0)
+ color2:vec4<f32> @offset(16), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %colors:ptr<__in, Colors, read> = var @location(1)
+}
+
+%foo = func():void {
+ $B2: {
+ %3:ptr<__in, vec4<f32>, read> = access %colors, 0u
+ %4:vec4<f32> = load %3
+ %5:ptr<__in, vec4<f32>, read> = access %colors, 1u
+ %6:f32 = load_vector_element %5, 2u
+ %7:vec4<f32> = mul %4, %6
+ ret
+ }
+}
+%main = @fragment func():void {
+ $B3: {
+ %9:void = call %foo
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(perspective, centroid)
+}
+
+%foo = func(%colors:Colors):void {
+ $B1: {
+ %3:vec4<f32> = access %colors, 0u
+ %4:vec4<f32> = access %colors, 1u
+ %5:f32 = access %4, 2u
+ %6:vec4<f32> = mul %3, %5
+ ret
+ }
+}
+%main = @fragment func(%colors_1:Colors):void { # %colors_1: 'colors'
+ $B2: {
+ %9:void = call %foo, %colors_1
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_Struct_InterpolateOnVariable) {
+ auto* colors_str = ty.Struct(
+ mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ LocationAttrs(1),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ LocationAttrs(2u, core::Interpolation{core::InterpolationType::kLinear,
+ core::InterpolationSampling::kSample}),
+ },
+ });
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kIn, colors_str));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid};
+ colors->SetAttributes(attributes);
+ }
+ mod.root_block->Append(colors);
+
+ auto* foo = b.Function("foo", ty.void_());
+ b.Append(foo->Block(), [&] {
+ auto* ptr = ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>());
+ auto* color1_value = b.Load(b.Access(ptr, colors, 0_u));
+ auto* color2_z_value = b.LoadVectorElement(b.Access(ptr, colors, 1_u), 2_u);
+ b.Multiply(ty.vec4<f32>(), color1_value, color2_z_value);
+ b.Return(foo);
+ });
+
+ auto* ep = b.Function("main", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ b.Call(foo);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(linear, sample)
+}
+
+$B1: { # root
+ %colors:ptr<__in, Colors, read> = var @interpolate(perspective, centroid)
+}
+
+%foo = func():void {
+ $B2: {
+ %3:ptr<__in, vec4<f32>, read> = access %colors, 0u
+ %4:vec4<f32> = load %3
+ %5:ptr<__in, vec4<f32>, read> = access %colors, 1u
+ %6:f32 = load_vector_element %5, 2u
+ %7:vec4<f32> = mul %4, %6
+ ret
+ }
+}
+%main = @fragment func():void {
+ $B3: {
+ %9:void = call %foo
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1), @interpolate(perspective, centroid)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(linear, sample)
+}
+
+%foo = func(%colors:Colors):void {
+ $B1: {
+ %3:vec4<f32> = access %colors, 0u
+ %4:vec4<f32> = access %colors, 1u
+ %5:f32 = access %4, 2u
+ %6:vec4<f32> = mul %3, %5
+ ret
+ }
+}
+%main = @fragment func(%colors_1:Colors):void { # %colors_1: 'colors'
+ $B2: {
+ %9:void = call %foo, %colors_1
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_Struct_LoadWholeStruct) {
+ auto* colors_str = ty.Struct(
+ mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ LocationAttrs(1),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ LocationAttrs(2u, core::Interpolation{core::InterpolationType::kLinear,
+ core::InterpolationSampling::kCentroid}),
+ },
+ });
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kIn, colors_str));
+ mod.root_block->Append(colors);
+
+ auto* foo = b.Function("foo", ty.void_());
+ b.Append(foo->Block(), [&] {
+ auto* load = b.Load(colors);
+ auto* color1_value = b.Access<vec4<f32>>(load, 0_u);
+ auto* color2_z_value = b.Access<f32>(load, 1_u, 2_u);
+ b.Multiply(ty.vec4<f32>(), color1_value, color2_z_value);
+ b.Return(foo);
+ });
+
+ auto* ep = b.Function("main", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ b.Call(foo);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(linear, centroid)
+}
+
+$B1: { # root
+ %colors:ptr<__in, Colors, read> = var
+}
+
+%foo = func():void {
+ $B2: {
+ %3:Colors = load %colors
+ %4:vec4<f32> = access %3, 0u
+ %5:f32 = access %3, 1u, 2u
+ %6:vec4<f32> = mul %4, %5
+ ret
+ }
+}
+%main = @fragment func():void {
+ $B3: {
+ %8:void = call %foo
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(linear, centroid)
+}
+
+%foo = func(%colors:Colors):void {
+ $B1: {
+ %3:vec4<f32> = access %colors, 0u
+ %4:f32 = access %colors, 1u, 2u
+ %5:vec4<f32> = mul %3, %4
+ ret
+ }
+}
+%main = @fragment func(%colors_1:Colors):void { # %colors_1: 'colors'
+ $B2: {
+ %8:void = call %foo, %colors_1
+ ret
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, SingleOutput_Builtin) {
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ position->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(position);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep->Block(), [&] { //
+ b.Store(position, b.Splat<vec4<f32>>(1_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %position:ptr<__out, vec4<f32>, read_write> = var @builtin(position)
+}
+
+%foo = @vertex func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %position:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ ret
+ }
+}
+%foo = @vertex func():vec4<f32> [@position] {
+ $B3: {
+ %4:void = call %foo_inner
+ %5:vec4<f32> = load %position
+ ret %5
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, SingleOutput_Builtin_WithInvariant) {
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ attributes.invariant = true;
+ position->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(position);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep->Block(), [&] { //
+ b.Store(position, b.Splat<vec4<f32>>(1_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %position:ptr<__out, vec4<f32>, read_write> = var @invariant @builtin(position)
+}
+
+%foo = @vertex func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %position:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ ret
+ }
+}
+%foo = @vertex func():vec4<f32> [@invariant, @position] {
+ $B3: {
+ %4:void = call %foo_inner
+ %5:vec4<f32> = load %position
+ ret %5
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, SingleOutput_Location) {
+ auto* color = b.Var("color", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ color->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(color);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] { //
+ b.Store(color, b.Splat<vec4<f32>>(1_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %color:ptr<__out, vec4<f32>, read_write> = var @location(1)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %color:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ ret
+ }
+}
+%foo = @fragment func():vec4<f32> [@location(1)] {
+ $B3: {
+ %4:void = call %foo_inner
+ %5:vec4<f32> = load %color
+ ret %5
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, SingleOutput_Location_WithInterpolation) {
+ auto* color = b.Var("color", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid};
+ color->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(color);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] { //
+ b.Store(color, b.Splat<vec4<f32>>(1_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %color:ptr<__out, vec4<f32>, read_write> = var @location(1) @interpolate(perspective, centroid)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %color:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ ret
+ }
+}
+%foo = @fragment func():vec4<f32> [@location(1), @interpolate(perspective, centroid)] {
+ $B3: {
+ %4:void = call %foo_inner
+ %5:vec4<f32> = load %color
+ ret %5
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, MultipleOutputs) {
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ attributes.invariant = true;
+ position->SetAttributes(std::move(attributes));
+ }
+ auto* color1 = b.Var("color1", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ color1->SetAttributes(std::move(attributes));
+ }
+ auto* color2 = b.Var("color2", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid};
+ color2->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(position);
+ mod.root_block->Append(color1);
+ mod.root_block->Append(color2);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep->Block(), [&] { //
+ b.Store(position, b.Splat<vec4<f32>>(1_f, 4));
+ b.Store(color1, b.Splat<vec4<f32>>(0.5_f, 4));
+ b.Store(color2, b.Splat<vec4<f32>>(0.25_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %position:ptr<__out, vec4<f32>, read_write> = var @invariant @builtin(position)
+ %color1:ptr<__out, vec4<f32>, read_write> = var @location(1)
+ %color2:ptr<__out, vec4<f32>, read_write> = var @location(1) @interpolate(perspective, centroid)
+}
+
+%foo = @vertex func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ store %color1, vec4<f32>(0.5f)
+ store %color2, vec4<f32>(0.25f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_symbol = struct @align(16) {
+ position:vec4<f32> @offset(0), @invariant, @builtin(position)
+ color1:vec4<f32> @offset(16), @location(1)
+ color2:vec4<f32> @offset(32), @location(1), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %position:ptr<private, vec4<f32>, read_write> = var
+ %color1:ptr<private, vec4<f32>, read_write> = var
+ %color2:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ store %color1, vec4<f32>(0.5f)
+ store %color2, vec4<f32>(0.25f)
+ ret
+ }
+}
+%foo = @vertex func():tint_symbol {
+ $B3: {
+ %6:void = call %foo_inner
+ %7:vec4<f32> = load %position
+ %8:vec4<f32> = load %color1
+ %9:vec4<f32> = load %color2
+ %10:tint_symbol = construct %7, %8, %9
+ ret %10
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Outputs_Struct_LocationOnEachMember) {
+ auto* builtin_str =
+ ty.Struct(mod.symbols.New("Builtins"), Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("position"),
+ ty.vec4<f32>(),
+ BuiltinAttrs(core::BuiltinValue::kPosition),
+ },
+ });
+ auto* colors_str = ty.Struct(
+ mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ LocationAttrs(1),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ LocationAttrs(2u, core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid}),
+ },
+ });
+
+ auto* builtins = b.Var("builtins", ty.ptr(core::AddressSpace::kOut, builtin_str));
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kOut, colors_str));
+ mod.root_block->Append(builtins);
+ mod.root_block->Append(colors);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep->Block(), [&] { //
+ auto* ptr = ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>());
+ b.Store(b.Access(ptr, builtins, 0_u), b.Splat<vec4<f32>>(1_f, 4));
+ b.Store(b.Access(ptr, colors, 0_u), b.Splat<vec4<f32>>(0.5_f, 4));
+ b.Store(b.Access(ptr, colors, 1_u), b.Splat<vec4<f32>>(0.25_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Builtins = struct @align(16) {
+ position:vec4<f32> @offset(0), @builtin(position)
+}
+
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(1)
+ color2:vec4<f32> @offset(16), @location(2), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %builtins:ptr<__out, Builtins, read_write> = var
+ %colors:ptr<__out, Colors, read_write> = var
+}
+
+%foo = @vertex func():void {
+ $B2: {
+ %4:ptr<__out, vec4<f32>, read_write> = access %builtins, 0u
+ store %4, vec4<f32>(1.0f)
+ %5:ptr<__out, vec4<f32>, read_write> = access %colors, 0u
+ store %5, vec4<f32>(0.5f)
+ %6:ptr<__out, vec4<f32>, read_write> = access %colors, 1u
+ store %6, vec4<f32>(0.25f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Builtins = struct @align(16) {
+ position:vec4<f32> @offset(0)
+}
+
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0)
+ color2:vec4<f32> @offset(16)
+}
+
+tint_symbol = struct @align(16) {
+ position:vec4<f32> @offset(0), @builtin(position)
+ color1:vec4<f32> @offset(16), @location(1)
+ color2:vec4<f32> @offset(32), @location(2), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %builtins:ptr<private, Builtins, read_write> = var
+ %colors:ptr<private, Colors, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ %4:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+ store %4, vec4<f32>(1.0f)
+ %5:ptr<private, vec4<f32>, read_write> = access %colors, 0u
+ store %5, vec4<f32>(0.5f)
+ %6:ptr<private, vec4<f32>, read_write> = access %colors, 1u
+ store %6, vec4<f32>(0.25f)
+ ret
+ }
+}
+%foo = @vertex func():tint_symbol {
+ $B3: {
+ %8:void = call %foo_inner
+ %9:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+ %10:vec4<f32> = load %9
+ %11:ptr<private, vec4<f32>, read_write> = access %colors, 0u
+ %12:vec4<f32> = load %11
+ %13:ptr<private, vec4<f32>, read_write> = access %colors, 1u
+ %14:vec4<f32> = load %13
+ %15:tint_symbol = construct %10, %12, %14
+ ret %15
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Outputs_Struct_LocationOnVariable) {
+ auto* builtin_str =
+ ty.Struct(mod.symbols.New("Builtins"), Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("position"),
+ ty.vec4<f32>(),
+ BuiltinAttrs(core::BuiltinValue::kPosition),
+ },
+ });
+ auto* colors_str =
+ ty.Struct(mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ core::type::StructMemberAttributes{
+ /* location */ std::nullopt,
+ /* index */ std::nullopt,
+ /* color */ std::nullopt,
+ /* builtin */ std::nullopt,
+ /* interpolation */
+ core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid},
+ /* invariant */ false,
+ },
+ },
+ });
+
+ auto* builtins = b.Var("builtins", ty.ptr(core::AddressSpace::kOut, builtin_str));
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kOut, colors_str));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ colors->SetAttributes(attributes);
+ }
+ mod.root_block->Append(builtins);
+ mod.root_block->Append(colors);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep->Block(), [&] { //
+ auto* ptr = ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>());
+ b.Store(b.Access(ptr, builtins, 0_u), b.Splat<vec4<f32>>(1_f, 4));
+ b.Store(b.Access(ptr, colors, 0_u), b.Splat<vec4<f32>>(0.5_f, 4));
+ b.Store(b.Access(ptr, colors, 1_u), b.Splat<vec4<f32>>(0.25_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Builtins = struct @align(16) {
+ position:vec4<f32> @offset(0), @builtin(position)
+}
+
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0)
+ color2:vec4<f32> @offset(16), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %builtins:ptr<__out, Builtins, read_write> = var
+ %colors:ptr<__out, Colors, read_write> = var @location(1)
+}
+
+%foo = @vertex func():void {
+ $B2: {
+ %4:ptr<__out, vec4<f32>, read_write> = access %builtins, 0u
+ store %4, vec4<f32>(1.0f)
+ %5:ptr<__out, vec4<f32>, read_write> = access %colors, 0u
+ store %5, vec4<f32>(0.5f)
+ %6:ptr<__out, vec4<f32>, read_write> = access %colors, 1u
+ store %6, vec4<f32>(0.25f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Builtins = struct @align(16) {
+ position:vec4<f32> @offset(0)
+}
+
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0)
+ color2:vec4<f32> @offset(16)
+}
+
+tint_symbol = struct @align(16) {
+ position:vec4<f32> @offset(0), @builtin(position)
+ color1:vec4<f32> @offset(16), @location(1)
+ color2:vec4<f32> @offset(32), @location(2), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %builtins:ptr<private, Builtins, read_write> = var
+ %colors:ptr<private, Colors, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ %4:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+ store %4, vec4<f32>(1.0f)
+ %5:ptr<private, vec4<f32>, read_write> = access %colors, 0u
+ store %5, vec4<f32>(0.5f)
+ %6:ptr<private, vec4<f32>, read_write> = access %colors, 1u
+ store %6, vec4<f32>(0.25f)
+ ret
+ }
+}
+%foo = @vertex func():tint_symbol {
+ $B3: {
+ %8:void = call %foo_inner
+ %9:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+ %10:vec4<f32> = load %9
+ %11:ptr<private, vec4<f32>, read_write> = access %colors, 0u
+ %12:vec4<f32> = load %11
+ %13:ptr<private, vec4<f32>, read_write> = access %colors, 1u
+ %14:vec4<f32> = load %13
+ %15:tint_symbol = construct %10, %12, %14
+ ret %15
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Outputs_Struct_InterpolateOnVariable) {
+ auto* builtin_str =
+ ty.Struct(mod.symbols.New("Builtins"), Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("position"),
+ ty.vec4<f32>(),
+ BuiltinAttrs(core::BuiltinValue::kPosition),
+ },
+ });
+ auto* colors_str =
+ ty.Struct(mod.symbols.New("Colors"),
+ Vector{
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color1"),
+ ty.vec4<f32>(),
+ LocationAttrs(2),
+ },
+ core::type::Manager::StructMemberDesc{
+ mod.symbols.New("color2"),
+ ty.vec4<f32>(),
+ LocationAttrs(3, core::Interpolation{core::InterpolationType::kFlat}),
+ },
+ });
+
+ auto* builtins = b.Var("builtins", ty.ptr(core::AddressSpace::kOut, builtin_str));
+ auto* colors = b.Var("colors", ty.ptr(core::AddressSpace::kOut, colors_str));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid};
+ colors->SetAttributes(attributes);
+ }
+ mod.root_block->Append(builtins);
+ mod.root_block->Append(colors);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep->Block(), [&] { //
+ auto* ptr = ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>());
+ b.Store(b.Access(ptr, builtins, 0_u), b.Splat<vec4<f32>>(1_f, 4));
+ b.Store(b.Access(ptr, colors, 0_u), b.Splat<vec4<f32>>(0.5_f, 4));
+ b.Store(b.Access(ptr, colors, 1_u), b.Splat<vec4<f32>>(0.25_f, 4));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+Builtins = struct @align(16) {
+ position:vec4<f32> @offset(0), @builtin(position)
+}
+
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0), @location(2)
+ color2:vec4<f32> @offset(16), @location(3), @interpolate(flat)
+}
+
+$B1: { # root
+ %builtins:ptr<__out, Builtins, read_write> = var
+ %colors:ptr<__out, Colors, read_write> = var @interpolate(perspective, centroid)
+}
+
+%foo = @vertex func():void {
+ $B2: {
+ %4:ptr<__out, vec4<f32>, read_write> = access %builtins, 0u
+ store %4, vec4<f32>(1.0f)
+ %5:ptr<__out, vec4<f32>, read_write> = access %colors, 0u
+ store %5, vec4<f32>(0.5f)
+ %6:ptr<__out, vec4<f32>, read_write> = access %colors, 1u
+ store %6, vec4<f32>(0.25f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+Builtins = struct @align(16) {
+ position:vec4<f32> @offset(0)
+}
+
+Colors = struct @align(16) {
+ color1:vec4<f32> @offset(0)
+ color2:vec4<f32> @offset(16)
+}
+
+tint_symbol = struct @align(16) {
+ position:vec4<f32> @offset(0), @builtin(position)
+ color1:vec4<f32> @offset(16), @location(2), @interpolate(perspective, centroid)
+ color2:vec4<f32> @offset(32), @location(3), @interpolate(flat)
+}
+
+$B1: { # root
+ %builtins:ptr<private, Builtins, read_write> = var
+ %colors:ptr<private, Colors, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ %4:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+ store %4, vec4<f32>(1.0f)
+ %5:ptr<private, vec4<f32>, read_write> = access %colors, 0u
+ store %5, vec4<f32>(0.5f)
+ %6:ptr<private, vec4<f32>, read_write> = access %colors, 1u
+ store %6, vec4<f32>(0.25f)
+ ret
+ }
+}
+%foo = @vertex func():tint_symbol {
+ $B3: {
+ %8:void = call %foo_inner
+ %9:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+ %10:vec4<f32> = load %9
+ %11:ptr<private, vec4<f32>, read_write> = access %colors, 0u
+ %12:vec4<f32> = load %11
+ %13:ptr<private, vec4<f32>, read_write> = access %colors, 1u
+ %14:vec4<f32> = load %13
+ %15:tint_symbol = construct %10, %12, %14
+ ret %15
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Outputs_UsedByMultipleEntryPoints) {
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ attributes.invariant = true;
+ position->SetAttributes(std::move(attributes));
+ }
+ auto* color1 = b.Var("color1", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ color1->SetAttributes(std::move(attributes));
+ }
+ auto* color2 = b.Var("color2", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ attributes.interpolation = core::Interpolation{core::InterpolationType::kPerspective,
+ core::InterpolationSampling::kCentroid};
+ color2->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(position);
+ mod.root_block->Append(color1);
+ mod.root_block->Append(color2);
+
+ auto* ep1 = b.Function("main1", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep1->Block(), [&] { //
+ b.Store(position, b.Splat<vec4<f32>>(1_f, 4));
+ b.Return(ep1);
+ });
+
+ auto* ep2 = b.Function("main2", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep2->Block(), [&] { //
+ b.Store(position, b.Splat<vec4<f32>>(1_f, 4));
+ b.Store(color1, b.Splat<vec4<f32>>(0.5_f, 4));
+ b.Return(ep2);
+ });
+
+ auto* ep3 = b.Function("main3", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+ b.Append(ep3->Block(), [&] { //
+ b.Store(position, b.Splat<vec4<f32>>(1_f, 4));
+ b.Store(color2, b.Splat<vec4<f32>>(0.25_f, 4));
+ b.Return(ep3);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %position:ptr<__out, vec4<f32>, read_write> = var @invariant @builtin(position)
+ %color1:ptr<__out, vec4<f32>, read_write> = var @location(1)
+ %color2:ptr<__out, vec4<f32>, read_write> = var @location(1) @interpolate(perspective, centroid)
+}
+
+%main1 = @vertex func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ ret
+ }
+}
+%main2 = @vertex func():void {
+ $B3: {
+ store %position, vec4<f32>(1.0f)
+ store %color1, vec4<f32>(0.5f)
+ ret
+ }
+}
+%main3 = @vertex func():void {
+ $B4: {
+ store %position, vec4<f32>(1.0f)
+ store %color2, vec4<f32>(0.25f)
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_symbol = struct @align(16) {
+ position:vec4<f32> @offset(0), @invariant, @builtin(position)
+ color1:vec4<f32> @offset(16), @location(1)
+}
+
+tint_symbol_1 = struct @align(16) {
+ position:vec4<f32> @offset(0), @invariant, @builtin(position)
+ color2:vec4<f32> @offset(16), @location(1), @interpolate(perspective, centroid)
+}
+
+$B1: { # root
+ %position:ptr<private, vec4<f32>, read_write> = var
+ %color1:ptr<private, vec4<f32>, read_write> = var
+ %color2:ptr<private, vec4<f32>, read_write> = var
+}
+
+%main1_inner = func():void {
+ $B2: {
+ store %position, vec4<f32>(1.0f)
+ ret
+ }
+}
+%main2_inner = func():void {
+ $B3: {
+ store %position, vec4<f32>(1.0f)
+ store %color1, vec4<f32>(0.5f)
+ ret
+ }
+}
+%main3_inner = func():void {
+ $B4: {
+ store %position, vec4<f32>(1.0f)
+ store %color2, vec4<f32>(0.25f)
+ ret
+ }
+}
+%main1 = @vertex func():vec4<f32> [@invariant, @position] {
+ $B5: {
+ %8:void = call %main1_inner
+ %9:vec4<f32> = load %position
+ ret %9
+ }
+}
+%main2 = @vertex func():tint_symbol {
+ $B6: {
+ %11:void = call %main2_inner
+ %12:vec4<f32> = load %position
+ %13:vec4<f32> = load %color1
+ %14:tint_symbol = construct %12, %13
+ ret %14
+ }
+}
+%main3 = @vertex func():tint_symbol_1 {
+ $B7: {
+ %16:void = call %main3_inner
+ %17:vec4<f32> = load %position
+ %18:vec4<f32> = load %color2
+ %19:tint_symbol_1 = construct %17, %18
+ ret %19
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Output_LoadAndStore) {
+ auto* color = b.Var("color", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ color->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(color);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] { //
+ b.Store(color, b.Splat<vec4<f32>>(1_f, 4));
+ auto* load = b.Load(color);
+ auto* mul = b.Multiply<vec4<f32>>(load, 2_f);
+ b.Store(color, mul);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %color:ptr<__out, vec4<f32>, read_write> = var @location(1)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ %3:vec4<f32> = load %color
+ %4:vec4<f32> = mul %3, 2.0f
+ store %color, %4
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %color:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ %3:vec4<f32> = load %color
+ %4:vec4<f32> = mul %3, 2.0f
+ store %color, %4
+ ret
+ }
+}
+%foo = @fragment func():vec4<f32> [@location(1)] {
+ $B3: {
+ %6:void = call %foo_inner
+ %7:vec4<f32> = load %color
+ ret %7
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Output_LoadVectorElementAndStoreVectorElement) {
+ auto* color = b.Var("color", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ color->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(color);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] { //
+ b.Store(color, b.Splat<vec4<f32>>(1_f, 4));
+ auto* load = b.LoadVectorElement(color, 2_u);
+ auto* mul = b.Multiply<f32>(load, 2_f);
+ b.StoreVectorElement(color, 2_u, mul);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %color:ptr<__out, vec4<f32>, read_write> = var @location(1)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ %3:f32 = load_vector_element %color, 2u
+ %4:f32 = mul %3, 2.0f
+ store_vector_element %color, 2u, %4
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %color:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ store %color, vec4<f32>(1.0f)
+ %3:f32 = load_vector_element %color, 2u
+ %4:f32 = mul %3, 2.0f
+ store_vector_element %color, 2u, %4
+ ret
+ }
+}
+%foo = @fragment func():vec4<f32> [@location(1)] {
+ $B3: {
+ %6:void = call %foo_inner
+ %7:vec4<f32> = load %color
+ ret %7
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Output_AccessChain) {
+ auto* color = b.Var("color", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1u;
+ color->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(color);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] { //
+ auto* access_1 = b.Access(ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()), color);
+ auto* access_2 = b.Access(ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()), access_1);
+ auto* load = b.LoadVectorElement(access_2, 2_u);
+ auto* mul = b.Multiply<vec4<f32>>(b.Splat<vec4<f32>>(1_f, 4), load);
+ b.Store(access_2, mul);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %color:ptr<__out, vec4<f32>, read_write> = var @location(1)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ %3:ptr<__out, vec4<f32>, read_write> = access %color
+ %4:ptr<__out, vec4<f32>, read_write> = access %3
+ %5:f32 = load_vector_element %4, 2u
+ %6:vec4<f32> = mul vec4<f32>(1.0f), %5
+ store %4, %6
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %color:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func():void {
+ $B2: {
+ %3:ptr<private, vec4<f32>, read_write> = access %color
+ %4:ptr<private, vec4<f32>, read_write> = access %3
+ %5:f32 = load_vector_element %4, 2u
+ %6:vec4<f32> = mul vec4<f32>(1.0f), %5
+ store %4, %6
+ ret
+ }
+}
+%foo = @fragment func():vec4<f32> [@location(1)] {
+ $B3: {
+ %8:void = call %foo_inner
+ %9:vec4<f32> = load %color
+ ret %9
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Inputs_And_Outputs) {
+ auto* position = b.Var("position", ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kPosition;
+ attributes.invariant = true;
+ position->SetAttributes(std::move(attributes));
+ }
+ auto* color_in = b.Var("color_in", ty.ptr(core::AddressSpace::kIn, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 0;
+ color_in->SetAttributes(std::move(attributes));
+ }
+ auto* color_out_1 = b.Var("color_out_1", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 1;
+ color_out_1->SetAttributes(std::move(attributes));
+ }
+ auto* color_out_2 = b.Var("color_out_2", ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>()));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.location = 2;
+ color_out_2->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(position);
+ mod.root_block->Append(color_in);
+ mod.root_block->Append(color_out_1);
+ mod.root_block->Append(color_out_2);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ auto* position_value = b.Load(position);
+ auto* color_in_value = b.Load(color_in);
+ b.Store(color_out_1, position_value);
+ b.Store(color_out_2, color_in_value);
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %position:ptr<__in, vec4<f32>, read> = var @invariant @builtin(position)
+ %color_in:ptr<__in, vec4<f32>, read> = var @location(0)
+ %color_out_1:ptr<__out, vec4<f32>, read_write> = var @location(1)
+ %color_out_2:ptr<__out, vec4<f32>, read_write> = var @location(2)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ %6:vec4<f32> = load %position
+ %7:vec4<f32> = load %color_in
+ store %color_out_1, %6
+ store %color_out_2, %7
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_symbol = struct @align(16) {
+ color_out_1:vec4<f32> @offset(0), @location(1)
+ color_out_2:vec4<f32> @offset(16), @location(2)
+}
+
+$B1: { # root
+ %color_out_1:ptr<private, vec4<f32>, read_write> = var
+ %color_out_2:ptr<private, vec4<f32>, read_write> = var
+}
+
+%foo_inner = func(%position:vec4<f32>, %color_in:vec4<f32>):void {
+ $B2: {
+ store %color_out_1, %position
+ store %color_out_2, %color_in
+ ret
+ }
+}
+%foo = @fragment func(%position_1:vec4<f32> [@invariant, @position], %color_in_1:vec4<f32> [@location(0)]):tint_symbol { # %position_1: 'position', %color_in_1: 'color_in'
+ $B3: {
+ %9:void = call %foo_inner, %position_1, %color_in_1
+ %10:vec4<f32> = load %color_out_1
+ %11:vec4<f32> = load %color_out_2
+ %12:tint_symbol = construct %10, %11
+ ret %12
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+// Test that a sample mask array is converted to a scalar u32 for the entry point.
+TEST_F(SpirvReader_ShaderIOTest, SampleMask) {
+ auto* arr = ty.array<u32, 1>();
+ auto* mask_in = b.Var("mask_in", ty.ptr(core::AddressSpace::kIn, arr));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kSampleMask;
+ mask_in->SetAttributes(std::move(attributes));
+ }
+ auto* mask_out = b.Var("mask_out", ty.ptr(core::AddressSpace::kOut, arr));
+ {
+ core::ir::IOAttributes attributes;
+ attributes.builtin = core::BuiltinValue::kSampleMask;
+ mask_out->SetAttributes(std::move(attributes));
+ }
+ mod.root_block->Append(mask_in);
+ mod.root_block->Append(mask_out);
+
+ auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kFragment);
+ b.Append(ep->Block(), [&] {
+ auto* mask_value = b.Load(mask_in);
+ auto* doubled = b.Multiply(ty.u32(), b.Access(ty.u32(), mask_value, 0_u), 2_u);
+ b.Store(mask_out, b.Construct(arr, doubled));
+ b.Return(ep);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %mask_in:ptr<__in, array<u32, 1>, read> = var @builtin(sample_mask)
+ %mask_out:ptr<__out, array<u32, 1>, read_write> = var @builtin(sample_mask)
+}
+
+%foo = @fragment func():void {
+ $B2: {
+ %4:array<u32, 1> = load %mask_in
+ %5:u32 = access %4, 0u
+ %6:u32 = mul %5, 2u
+ %7:array<u32, 1> = construct %6
+ store %mask_out, %7
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %mask_out:ptr<private, array<u32, 1>, read_write> = var
+}
+
+%foo_inner = func(%mask_in:array<u32, 1>):void {
+ $B2: {
+ %4:u32 = access %mask_in, 0u
+ %5:u32 = mul %4, 2u
+ %6:array<u32, 1> = construct %5
+ store %mask_out, %6
+ ret
+ }
+}
+%foo = @fragment func(%mask_in_1:u32 [@sample_mask]):u32 [@sample_mask] { # %mask_in_1: 'mask_in'
+ $B3: {
+ %9:array<u32, 1> = construct %mask_in_1
+ %10:void = call %foo_inner, %9
+ %11:array<u32, 1> = load %mask_out
+ %12:u32 = access %11, 0u
+ ret %12
+ }
+}
+)";
+
+ Run(ShaderIO);
+
+ EXPECT_EQ(expect, str());
+}
+
+} // namespace
+} // namespace tint::spirv::reader::lower
diff --git a/src/tint/lang/spirv/reader/reader_test.cc b/src/tint/lang/spirv/reader/reader_test.cc
index d8ab351..5242819 100644
--- a/src/tint/lang/spirv/reader/reader_test.cc
+++ b/src/tint/lang/spirv/reader/reader_test.cc
@@ -154,5 +154,201 @@
)");
}
+TEST_F(SpirvReaderTest, ShaderInputs) {
+ auto got = Run(R"(
+ OpCapability Shader
+ OpCapability SampleRateShading
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %main "main" %coord %colors
+ OpExecutionMode %main OriginUpperLeft
+ OpDecorate %coord BuiltIn FragCoord
+ OpDecorate %colors Location 1
+ OpMemberDecorate %str 1 NoPerspective
+ %void = OpTypeVoid
+ %f32 = OpTypeFloat 32
+ %vec4f = OpTypeVector %f32 4
+ %fn_type = OpTypeFunction %void
+ %str = OpTypeStruct %vec4f %vec4f
+ %u32 = OpTypeInt 32 0
+ %u32_0 = OpConstant %u32 0
+ %u32_1 = OpConstant %u32 1
+
+%_ptr_Input_vec4f = OpTypePointer Input %vec4f
+ %_ptr_Input_str = OpTypePointer Input %str
+ %coord = OpVariable %_ptr_Input_vec4f Input
+ %colors = OpVariable %_ptr_Input_str Input
+
+ %main = OpFunction %void None %fn_type
+ %main_start = OpLabel
+ %access_a = OpAccessChain %_ptr_Input_vec4f %colors %u32_0
+ %access_b = OpAccessChain %_ptr_Input_vec4f %colors %u32_1
+ %a = OpLoad %vec4f %access_a
+ %b = OpLoad %vec4f %access_b
+ %c = OpLoad %vec4f %coord
+ %mul = OpFMul %vec4f %a %b
+ %add = OpFAdd %vec4f %mul %c
+ OpReturn
+ OpFunctionEnd
+)");
+ ASSERT_EQ(got, Success);
+ EXPECT_EQ(got, R"(
+tint_symbol_2 = struct @align(16) {
+ tint_symbol:vec4<f32> @offset(0), @location(1)
+ tint_symbol_1:vec4<f32> @offset(16), @location(2), @interpolate(linear, center)
+}
+
+%main = @fragment func(%2:vec4<f32> [@position], %3:tint_symbol_2):void {
+ $B1: {
+ %4:vec4<f32> = access %3, 0u
+ %5:vec4<f32> = access %3, 1u
+ %6:vec4<f32> = mul %4, %5
+ %7:vec4<f32> = add %6, %2
+ ret
+ }
+}
+)");
+}
+
+TEST_F(SpirvReaderTest, ShaderOutputs) {
+ auto got = Run(R"(
+ OpCapability Shader
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %main "main" %depth %colors
+ OpExecutionMode %main OriginUpperLeft
+ OpExecutionMode %main DepthReplacing
+ OpDecorate %depth BuiltIn FragDepth
+ OpDecorate %colors Location 1
+ OpMemberDecorate %str 1 NoPerspective
+ %void = OpTypeVoid
+ %f32 = OpTypeFloat 32
+ %vec4f = OpTypeVector %f32 4
+ %fn_type = OpTypeFunction %void
+ %str = OpTypeStruct %vec4f %vec4f
+ %u32 = OpTypeInt 32 0
+ %u32_0 = OpConstant %u32 0
+ %u32_1 = OpConstant %u32 1
+ %f32_42 = OpConstant %f32 42.0
+ %f32_n1 = OpConstant %f32 -1.0
+ %f32_v4_a = OpConstantComposite %vec4f %f32_42 %f32_42 %f32_42 %f32_n1
+ %f32_v4_b = OpConstantComposite %vec4f %f32_n1 %f32_n1 %f32_n1 %f32_42
+
+%_ptr_Output_f32 = OpTypePointer Output %f32
+%_ptr_Output_vec4f = OpTypePointer Output %vec4f
+ %_ptr_Output_str = OpTypePointer Output %str
+ %depth = OpVariable %_ptr_Output_f32 Output
+ %colors = OpVariable %_ptr_Output_str Output
+
+ %main = OpFunction %void None %fn_type
+ %main_start = OpLabel
+ %access_a = OpAccessChain %_ptr_Output_vec4f %colors %u32_0
+ %access_b = OpAccessChain %_ptr_Output_vec4f %colors %u32_1
+ OpStore %access_a %f32_v4_a
+ OpStore %access_b %f32_v4_b
+ OpStore %depth %f32_42
+ OpReturn
+ OpFunctionEnd
+)");
+ ASSERT_EQ(got, Success);
+ EXPECT_EQ(got, R"(
+tint_symbol_2 = struct @align(16) {
+ tint_symbol:vec4<f32> @offset(0)
+ tint_symbol_1:vec4<f32> @offset(16)
+}
+
+tint_symbol_4 = struct @align(16) {
+ tint_symbol_3:f32 @offset(0), @builtin(frag_depth)
+ tint_symbol:vec4<f32> @offset(16), @location(1)
+ tint_symbol_1:vec4<f32> @offset(32), @location(2), @interpolate(linear, center)
+}
+
+$B1: { # root
+ %1:ptr<private, f32, read_write> = var
+ %2:ptr<private, tint_symbol_2, read_write> = var
+}
+
+%main_inner = func():void {
+ $B2: {
+ %4:ptr<private, vec4<f32>, read_write> = access %2, 0u
+ %5:ptr<private, vec4<f32>, read_write> = access %2, 1u
+ store %4, vec4<f32>(42.0f, 42.0f, 42.0f, -1.0f)
+ store %5, vec4<f32>(-1.0f, -1.0f, -1.0f, 42.0f)
+ store %1, 42.0f
+ ret
+ }
+}
+%main = @fragment func():tint_symbol_4 {
+ $B3: {
+ %7:void = call %main_inner
+ %8:f32 = load %1
+ %9:ptr<private, vec4<f32>, read_write> = access %2, 0u
+ %10:vec4<f32> = load %9
+ %11:ptr<private, vec4<f32>, read_write> = access %2, 1u
+ %12:vec4<f32> = load %11
+ %13:tint_symbol_4 = construct %8, %10, %12
+ ret %13
+ }
+}
+)");
+}
+
+TEST_F(SpirvReaderTest, SampleMask) {
+ auto got = Run(R"(
+ OpCapability Shader
+ OpMemoryModel Logical GLSL450
+ OpEntryPoint Fragment %main "main" %mask_in %mask_out
+ OpExecutionMode %main OriginUpperLeft
+ OpDecorate %mask_in BuiltIn SampleMask
+ OpDecorate %mask_out BuiltIn SampleMask
+ %void = OpTypeVoid
+ %fn_type = OpTypeFunction %void
+ %u32 = OpTypeInt 32 0
+ %u32_0 = OpConstant %u32 0
+ %u32_1 = OpConstant %u32 1
+ %arr_u32 = OpTypeArray %u32 %u32_1
+
+%_ptr_Input_u32 = OpTypePointer Input %u32
+%_ptr_Input_arr_u32 = OpTypePointer Input %arr_u32
+%_ptr_Output_u32 = OpTypePointer Output %u32
+%_ptr_Output_arr_u32 = OpTypePointer Output %arr_u32
+ %mask_in = OpVariable %_ptr_Input_arr_u32 Input
+ %mask_out = OpVariable %_ptr_Output_arr_u32 Output
+
+ %main = OpFunction %void None %fn_type
+ %main_start = OpLabel
+ %mask_in_0 = OpAccessChain %_ptr_Input_u32 %mask_in %u32_0
+%mask_in_val = OpLoad %u32 %mask_in_0
+ %plus_one = OpIAdd %u32 %mask_in_val %u32_1
+ %mask_out_0 = OpAccessChain %_ptr_Output_u32 %mask_out %u32_0
+ OpStore %mask_out_0 %plus_one
+ OpReturn
+ OpFunctionEnd
+)");
+ ASSERT_EQ(got, Success);
+ EXPECT_EQ(got, R"(
+$B1: { # root
+ %1:ptr<private, array<u32, 1>, read_write> = var
+}
+
+%main_inner = func(%3:array<u32, 1>):void {
+ $B2: {
+ %4:u32 = access %3, 0u
+ %5:u32 = add %4, 1u
+ %6:ptr<private, u32, read_write> = access %1, 0u
+ store %6, %5
+ ret
+ }
+}
+%main = @fragment func(%8:u32 [@sample_mask]):u32 [@sample_mask] {
+ $B3: {
+ %9:array<u32, 1> = construct %8
+ %10:void = call %main_inner, %9
+ %11:array<u32, 1> = load %1
+ %12:u32 = access %11, 0u
+ ret %12
+ }
+}
+)");
+}
+
} // namespace
} // namespace tint::spirv::reader