Add ArrayLengthFromImmediates transform
ArrayLengthFromUniform() is used to support arrayLength() built-in function.
After immediate constants supported, immediate block could hold array
length and support arrayLength() built-in.
This CL add ArrayLengthFromImmediates transform to support array length
from immediate block. And will replace ArrayLengthFromUniform in future.
Bug:366291600
Change-Id: I5e5c2df0385cd2f2e846b206b652b5e963f0aad0
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/252614
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index f7580cb..af40537 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -40,6 +40,7 @@
name = "transform",
srcs = [
"add_empty_entry_point.cc",
+ "array_length_from_immediate.cc",
"array_length_from_uniform.cc",
"bgra8unorm_polyfill.cc",
"binary_polyfill.cc",
@@ -73,6 +74,7 @@
],
hdrs = [
"add_empty_entry_point.h",
+ "array_length_from_immediate.h",
"array_length_from_uniform.h",
"bgra8unorm_polyfill.h",
"binary_polyfill.h",
@@ -134,6 +136,7 @@
alwayslink = True,
srcs = [
"add_empty_entry_point_test.cc",
+ "array_length_from_immediate_test.cc",
"array_length_from_uniform_test.cc",
"bgra8unorm_polyfill_test.cc",
"binary_polyfill_test.cc",
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index d143d6f..8a24efc 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -41,6 +41,8 @@
tint_add_target(tint_lang_core_ir_transform lib
lang/core/ir/transform/add_empty_entry_point.cc
lang/core/ir/transform/add_empty_entry_point.h
+ lang/core/ir/transform/array_length_from_immediate.cc
+ lang/core/ir/transform/array_length_from_immediate.h
lang/core/ir/transform/array_length_from_uniform.cc
lang/core/ir/transform/array_length_from_uniform.h
lang/core/ir/transform/bgra8unorm_polyfill.cc
@@ -135,6 +137,7 @@
################################################################################
tint_add_target(tint_lang_core_ir_transform_test test
lang/core/ir/transform/add_empty_entry_point_test.cc
+ lang/core/ir/transform/array_length_from_immediate_test.cc
lang/core/ir/transform/array_length_from_uniform_test.cc
lang/core/ir/transform/bgra8unorm_polyfill_test.cc
lang/core/ir/transform/binary_polyfill_test.cc
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index d969108..5e18225 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -47,6 +47,8 @@
sources = [
"add_empty_entry_point.cc",
"add_empty_entry_point.h",
+ "array_length_from_immediate.cc",
+ "array_length_from_immediate.h",
"array_length_from_uniform.cc",
"array_length_from_uniform.h",
"bgra8unorm_polyfill.cc",
@@ -135,6 +137,7 @@
tint_unittests_source_set("unittests") {
sources = [
"add_empty_entry_point_test.cc",
+ "array_length_from_immediate_test.cc",
"array_length_from_uniform_test.cc",
"bgra8unorm_polyfill_test.cc",
"binary_polyfill_test.cc",
diff --git a/src/tint/lang/core/ir/transform/array_length_from_immediate.cc b/src/tint/lang/core/ir/transform/array_length_from_immediate.cc
new file mode 100644
index 0000000..d5a549f
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/array_length_from_immediate.cc
@@ -0,0 +1,369 @@
+// Copyright 2025 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/core/ir/transform/array_length_from_immediate.h"
+
+#include <algorithm>
+#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/prepare_immediate_data.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+using namespace tint::core::fluent_types; // NOLINT
+using namespace tint::core::number_suffixes; // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// PIMPL state for the transform.
+struct State {
+ /// The IR module.
+ Module& ir;
+
+ /// Immediate data layout contains all immediate block info.
+ const core::ir::transform::ImmediateDataLayout& immediate_data_layout;
+
+ /// The offset in immediate block for buffer sizes array.
+ uint32_t buffer_sizes_offset = 0;
+
+ /// The total number of vec4s used to store buffer sizes provided in the immediate block.
+ uint32_t buffer_sizes_array_elements_num = 0;
+
+ /// The map from binding point to the element index which holds the size of that buffer.
+ const std::unordered_map<BindingPoint, uint32_t>& bindpoint_to_size_index;
+
+ /// The IR builder.
+ core::ir::Builder b{ir};
+
+ /// The type manager.
+ core::type::Manager& ty{ir.Types()};
+
+ /// The construct instruction that creates the array lengths structure in the entry point.
+ Construct* lengths_constructor = nullptr;
+
+ /// A map from an array function parameter to the function parameter that holds its length.
+ Hashmap<FunctionParam*, FunctionParam*, 8> array_param_to_length_param{};
+
+ /// A map from a function to the structure that holds all of the array lengths.
+ Hashmap<Function*, Value*, 8> function_to_lengths_structure{};
+
+ /// A list of structure members for the array lengths structure.
+ Vector<type::Manager::StructMemberDesc, 8> lengths_structure_members{};
+
+ /// A map from a binding point to its index in the array length structure.
+ Hashmap<BindingPoint, uint32_t, 8> bindpoint_to_length_member_index{};
+
+ /// An ordered list of binding points that map to the structure members.
+ struct BindingPointInfo {
+ BindingPoint binding_point{};
+ const type::Type* store_type = nullptr;
+ };
+ Vector<BindingPointInfo, 8> ordered_bindpoints{};
+
+ /// A map from block to its containing function.
+ Hashmap<core::ir::Block*, core::ir::Function*, 64> block_to_function{};
+
+ /// Process the module.
+ void Process() {
+ // Seed the block-to-function map with the function entry blocks.
+ // This is used to determine the owning function for any given instruction.
+ for (auto& func : ir.functions) {
+ block_to_function.Add(func->Block(), func);
+ }
+
+ // Look for and replace calls to the array length builtin.
+ for (auto* inst : ir.Instructions()) {
+ if (auto* call = inst->As<CoreBuiltinCall>()) {
+ if (call->Func() == BuiltinFn::kArrayLength) {
+ MaybeReplace(call);
+ }
+ }
+ }
+
+ // Create the lengths structure and update all of the places that need to use it.
+ // We can only do this after we have replaced all of the array length callsites, now that we
+ // know all of the structure members that we need.
+ CreateLengthsStructure();
+ }
+
+ /// Replace a call to an array length builtin, if the variable appears in the bindpoint map.
+ /// @param call the arrayLength call to replace
+ void MaybeReplace(CoreBuiltinCall* call) {
+ if (auto* length = GetComputedLength(call->Args()[0], call)) {
+ call->Result()->ReplaceAllUsesWith(length);
+ call->Destroy();
+ }
+ }
+
+ /// Get the computed length value for a runtime-sized array pointer.
+ /// @param ptr the pointer to the runtime-sized array
+ /// @param insertion_point the insertion point for new instructions
+ /// @returns the computed length, or nullptr if the original builtin should be used
+ Value* GetComputedLength(Value* ptr, Instruction* insertion_point) {
+ // Trace back from the value until we reach the originating variable.
+ while (true) {
+ if (auto* param = ptr->As<FunctionParam>()) {
+ // The length of an array pointer passed as a function parameter will be passed as
+ // an additional parameter to the function.
+ return GetArrayLengthParam(param);
+ }
+
+ if (auto* result = ptr->As<InstructionResult>()) {
+ if (auto* var = result->Instruction()->As<Var>()) {
+ // We found the originating variable, so compute its array length.
+ return ComputeArrayLength(var, insertion_point);
+ }
+ if (auto* access = result->Instruction()->As<Access>()) {
+ ptr = access->Object();
+ continue;
+ }
+ if (auto* let = result->Instruction()->As<Let>()) {
+ ptr = let->Value();
+ continue;
+ }
+ TINT_UNREACHABLE() << "unhandled source of a storage buffer pointer: "
+ << result->Instruction()->TypeInfo().name;
+ }
+ TINT_UNREACHABLE() << "unhandled source of a storage buffer pointer: "
+ << ptr->TypeInfo().name;
+ }
+ }
+
+ /// Get (or create) the array length parameter that corresponds to an array parameter.
+ /// @param array_param the array parameter
+ /// @returns the array length parameter
+ FunctionParam* GetArrayLengthParam(FunctionParam* array_param) {
+ return array_param_to_length_param.GetOrAdd(array_param, [&] {
+ // Add a new parameter to receive the array length.
+ auto* length = b.FunctionParam<u32>("tint_array_length");
+ array_param->Function()->AppendParam(length);
+
+ // Update callsites of this function to pass the array length to it.
+ array_param->Function()->ForEachUseUnsorted([&](core::ir::Usage use) {
+ if (auto* call = use.instruction->As<core::ir::UserCall>()) {
+ // Get the length of the array in the calling function and pass that.
+ auto* arg = call->Args()[array_param->Index()];
+ auto* len = GetComputedLength(arg, call);
+ if (!len) {
+ // The originating variable was not in the bindpoint map, so we need to call
+ // the original arrayLength builtin as the callee is expecting a value.
+ b.InsertBefore(call, [&] {
+ len = b.Call<u32>(BuiltinFn::kArrayLength, arg)->Result();
+ });
+ }
+ call->AppendArg(len);
+ }
+ });
+
+ return length;
+ });
+ }
+
+ /// Get (or create) the array lengths structure for a function.
+ /// @param func the function that needs the structure
+ /// @returns the array lengths structure
+ Value* GetArrayLengthsStructure(Function* func) {
+ return function_to_lengths_structure.GetOrAdd(func, [&]() -> Value* {
+ if (func->IsEntryPoint()) {
+ // Create a placeholder construct instruction for the lengths structure that will be
+ // filled in later when we know all of the structure members.
+ TINT_ASSERT(lengths_constructor == nullptr);
+ lengths_constructor = b.ConstructWithResult(ir.CreateValue<InstructionResult>());
+ lengths_constructor->InsertBefore(func->Block()->Front());
+ return lengths_constructor->Result();
+ }
+
+ // Add a new parameter to receive the array lengths structure.
+ // The type is a placeholder and will be filled in later when we create the struct type.
+ auto* lengths = b.FunctionParam("tint_array_lengths", nullptr);
+ func->AppendParam(lengths);
+
+ // Update callsites of this function to pass the structure to it.
+ func->ForEachUseUnsorted([&](core::ir::Usage use) {
+ if (auto* call = use.instruction->As<core::ir::UserCall>()) {
+ // Get the structure in the calling function and pass that.
+ auto* caller = ContainingFunction(call);
+ call->AppendArg(GetArrayLengthsStructure(caller));
+ }
+ });
+
+ return lengths;
+ });
+ }
+
+ /// Compute the array length of the runtime-sized array that is inside a storage buffer
+ /// variable. If the variable's binding point is not found in the bindpoint map, returns nullptr
+ /// to indicate that the original arrayLength builtin should be used instead.
+ ///
+ /// @param var the storage buffer variable that contains the runtime-sized array
+ /// @param insertion_point the insertion point for new instructions
+ /// @returns the length of the array, or nullptr if the original builtin should be used
+ Value* ComputeArrayLength(Var* var, Instruction* insertion_point) {
+ auto binding = var->BindingPoint();
+ TINT_ASSERT(binding);
+
+ auto idx_it = bindpoint_to_size_index.find(*binding);
+ if (idx_it == bindpoint_to_size_index.end()) {
+ // If the bindpoint_to_size_index map does not contain an entry for the storage buffer,
+ // then we preserve the arrayLength() call.
+ return nullptr;
+ }
+
+ // Get the index of the structure member that holds the length for this binding point,
+ // creating the structure member descriptor if necessary.
+ auto member_index = bindpoint_to_length_member_index.GetOrAdd(*binding, [&]() {
+ auto index = static_cast<uint32_t>(lengths_structure_members.Length());
+ auto name = "tint_array_length_" + std::to_string(binding->group) + "_" +
+ std::to_string(binding->binding);
+ lengths_structure_members.Push(type::Manager::StructMemberDesc{
+ .name = ir.symbols.Register(name),
+ .type = ty.u32(),
+ });
+ ordered_bindpoints.Push(BindingPointInfo{
+ .binding_point = *binding,
+ .store_type = var->Result()->Type()->UnwrapPtr(),
+ });
+ return index;
+ });
+
+ // Extract the length from the structure.
+ auto* length = b.Access<u32>(GetArrayLengthsStructure(ContainingFunction(insertion_point)),
+ u32(member_index));
+ length->InsertBefore(insertion_point);
+ return length->Result();
+ }
+
+ /// Create the structure to hold the array lengths and fill in the construct instruction that
+ /// sets all of the length values.
+ void CreateLengthsStructure() {
+ if (lengths_structure_members.IsEmpty()) {
+ return;
+ }
+
+ // Create the lengths structure.
+ auto* lengths_struct = ty.Struct(ir.symbols.New("tint_array_lengths_struct"),
+ std::move(lengths_structure_members));
+
+ // Update all of the function parameters that need to receive the lengths structure.
+ for (auto s : function_to_lengths_structure) {
+ if (auto* param = s.value->As<FunctionParam>()) {
+ param->SetType(lengths_struct);
+ }
+ }
+
+ // Insert code at the beginning of the entry point to initialize the array length members.
+ if (lengths_constructor == nullptr) {
+ return;
+ }
+ lengths_constructor->Result()->SetType(lengths_struct);
+ b.InsertBefore(lengths_constructor->Block()->Front(), [&] {
+ Vector<Value*, 8> constructor_values;
+ for (auto info : ordered_bindpoints) {
+ TINT_ASSERT(bindpoint_to_size_index.contains(info.binding_point));
+ TINT_ASSERT(bindpoint_to_length_member_index.Contains(info.binding_point));
+
+ // Load the total storage buffer size from the immediate block.
+ // The sizes are packed into vec4s to satisfy the 16-byte alignment requirement for
+ // array elements in immediate block, so we have to find the vector and element that
+ // correspond to the index that we want.
+ const uint32_t size_index = bindpoint_to_size_index.at(info.binding_point);
+ const uint32_t array_index = size_index / 4;
+ const uint32_t vec_index = size_index % 4;
+ auto* buffer_sizes = b.Access(
+ ty.ptr(immediate, ty.array(ty.vec4<u32>(), buffer_sizes_array_elements_num)),
+ immediate_data_layout.var,
+ u32(immediate_data_layout.IndexOf(buffer_sizes_offset)));
+ auto* vec_ptr = b.Access(ty.ptr(immediate, ty.vec4<u32>()), buffer_sizes->Result(),
+ u32(array_index));
+ auto* total_buffer_size = b.LoadVectorElement(vec_ptr, u32(vec_index))->Result();
+
+ // Calculate actual array length:
+ // total_buffer_size - array_offset
+ // array_length = --------------------------------
+ // array_stride
+ auto* array_size = total_buffer_size;
+ const type::Array* array_type = nullptr;
+ if (auto* str = info.store_type->As<core::type::Struct>()) {
+ // The variable is a struct, so subtract the byte offset of the array member.
+ auto* member = str->Members().Back();
+ array_type = member->Type()->As<core::type::Array>();
+ array_size =
+ b.Subtract<u32>(total_buffer_size, u32(member->Offset()))->Result();
+ } else {
+ array_type = info.store_type->As<core::type::Array>();
+ }
+ TINT_ASSERT(array_type);
+
+ auto* length = b.Divide<u32>(array_size, u32(array_type->Stride()))->Result();
+ constructor_values.Push(length);
+ }
+ lengths_constructor->SetOperands(std::move(constructor_values));
+ });
+ }
+
+ /// 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());
+ });
+ }
+
+ /// @returns true if the transformed module needs a storage buffer sizes UBO
+ bool NeedsStorageBufferSizes() {
+ return !lengths_structure_members.IsEmpty() && lengths_constructor != nullptr;
+ }
+};
+
+} // namespace
+
+Result<ArrayLengthFromImmediateResult> ArrayLengthFromImmediates(
+ Module& ir,
+ const core::ir::transform::ImmediateDataLayout& immediate_data_layout,
+ const uint32_t buffer_sizes_offset,
+ const uint32_t buffer_sizes_array_elements_num,
+ const std::unordered_map<BindingPoint, uint32_t>& bindpoint_to_size_index) {
+ auto validated = ValidateAndDumpIfNeeded(ir, "core.ArrayLengthFromImmediates",
+ kArrayLengthFromImmediateCapabilities);
+ if (validated != Success) {
+ return validated.Failure();
+ }
+
+ State state{ir, immediate_data_layout, buffer_sizes_offset, buffer_sizes_array_elements_num,
+ bindpoint_to_size_index};
+ state.Process();
+
+ ArrayLengthFromImmediateResult result;
+ result.needs_storage_buffer_sizes = state.NeedsStorageBufferSizes();
+ return result;
+}
+
+} // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/array_length_from_immediate.h b/src/tint/lang/core/ir/transform/array_length_from_immediate.h
new file mode 100644
index 0000000..eeacc41
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/array_length_from_immediate.h
@@ -0,0 +1,89 @@
+// Copyright 2025 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_CORE_IR_TRANSFORM_ARRAY_LENGTH_FROM_IMMEDIATE_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_ARRAY_LENGTH_FROM_IMMEDIATE_H_
+
+#include <unordered_map>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/utils/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+struct ImmediateDataLayout;
+
+/// The capabilities that the transform can support.
+const Capabilities kArrayLengthFromImmediateCapabilities{Capability::kAllowDuplicateBindings};
+
+/// The result of running the ArrayLengthFromUniform transform.
+struct ArrayLengthFromImmediateResult {
+ /// `true` if the transformed module needs the storage buffer sizes immediate data.
+ bool needs_storage_buffer_sizes = false;
+};
+
+/// ArrayLengthFromImmediates is a transform that replaces calls to the arrayLength() builtin by
+/// calculating the array length from the total size of the storage buffer, which is received via
+/// immediate blocks.
+///
+/// The generated immediate blocks will have the form:
+/// ```
+/// @group(0) @binding(30)
+/// struct tint_immediate_data_struct {
+/// ...
+/// buffer_sizes: array<vec4<u32>, 8>; // offset is provided via config
+// };
+/// var<immediate> tint_immediate_data : tint_immediate_data_struct;
+/// ```
+/// The offset of `buffer_sizes` in the immediate block is provided by config.
+/// The transform config also defines the mapping from a storage buffer's `BindingPoint` to the
+/// element index that will be used to get the size of that buffer.
+///
+/// @param module the module to transform
+/// @param immediate_data_layout The immediate data layout information.
+/// @param bindpoint_to_size_index The map from binding point to an index which holds the size
+/// of that buffer.
+/// @param buffer_sizes_offset The offset in immediate block where buffer sizes start.
+/// @param buffer_sizes_array_elements_num the number of vec4s used to store buffer sizes that will
+/// be set into the immediate block.
+/// @returns the transform result or failure
+/// TODO(crbug.com/366291600): Replace ArrayLengthFromUniform.
+Result<ArrayLengthFromImmediateResult> ArrayLengthFromImmediates(
+ Module& module,
+ const core::ir::transform::ImmediateDataLayout& immediate_data_layout,
+ const uint32_t buffer_sizes_offset,
+ const uint32_t buffer_sizes_array_elements_num,
+ const std::unordered_map<BindingPoint, uint32_t>& bindpoint_to_size_index);
+
+} // namespace tint::core::ir::transform
+
+#endif // SRC_TINT_LANG_CORE_IR_TRANSFORM_ARRAY_LENGTH_FROM_IMMEDIATE_H_
diff --git a/src/tint/lang/core/ir/transform/array_length_from_immediate_test.cc b/src/tint/lang/core/ir/transform/array_length_from_immediate_test.cc
new file mode 100644
index 0000000..d289d9d
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/array_length_from_immediate_test.cc
@@ -0,0 +1,1580 @@
+// Copyright 2025 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/core/ir/transform/array_length_from_immediate.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+#include "src/tint/lang/core/ir/transform/prepare_immediate_data.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types; // NOLINT
+using namespace tint::core::number_suffixes; // NOLINT
+
+using IR_ArrayLengthFromImmediatesTest = TransformTest;
+
+uint32_t GetBufferSizesNumElements(
+ const std::unordered_map<BindingPoint, uint32_t>& bindpoint_to_size_index) {
+ uint32_t max_index = 0;
+ for (auto& entry : bindpoint_to_size_index) {
+ max_index = std::max(max_index, entry.second);
+ }
+ return (max_index / 4) + 1;
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, NoModify_UserFunction) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* user_func = b.Function("arrayLength", ty.u32());
+ auto* param = b.FunctionParam("arr", arr_ptr);
+ user_func->SetParams({param});
+ b.Append(user_func->Block(), [&] { //
+ b.Return(user_func, 42_u);
+ });
+
+ auto* func = b.Function("foo", ty.void_());
+ b.Append(func->Block(), [&] {
+ b.Call(user_func, buffer);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%arrayLength = func(%arr:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ ret 42u
+ }
+}
+%foo = func():void {
+ $B3: {
+ %5:u32 = call %arrayLength, %buffer
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%arrayLength = func(%arr:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ ret 42u
+ }
+}
+%foo = func():void {
+ $B3: {
+ %5:u32 = call %arrayLength, %buffer
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ auto immediate_data = PrepareImmediateData(mod, {});
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), 0u, 0u, bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, DirectUse) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, buffer);
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ %let:u32 = let %3
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %4:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %5:ptr<immediate, vec4<u32>, read> = access %4, 0u
+ %6:u32 = load_vector_element %5, 0u
+ %7:u32 = div %6, 4u
+ %8:tint_array_lengths_struct = construct %7
+ %9:u32 = access %8, 0u
+ %let:u32 = let %9
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, DirectUse_NonZeroIndex) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, buffer);
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ %let:u32 = let %3
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 2> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %4:ptr<immediate, array<vec4<u32>, 2>, read> = access %tint_immediate_data, 0u
+ %5:ptr<immediate, vec4<u32>, read> = access %4, 1u
+ %6:u32 = load_vector_element %5, 3u
+ %7:u32 = div %6, 4u
+ %8:tint_array_lengths_struct = construct %7
+ %9:u32 = access %8, 0u
+ %let:u32 = let %9
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 7;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, DirectUse_NotInMap) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 1);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, buffer);
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 1)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ %let:u32 = let %3
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 1)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ %let:u32 = let %3
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+
+ auto immediate_data = PrepareImmediateData(mod, {});
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), 0u, 0u, bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, DirectUse_NoEntryPoint) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ b.Append(bar->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, buffer);
+ b.Return(bar, len);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func():u32 {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ ret %3
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%tint_array_lengths:tint_array_lengths_struct):u32 {
+ $B2: {
+ %4:u32 = access %tint_array_lengths, 0u
+ ret %4
+ }
+}
+)";
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ auto immediate_data = PrepareImmediateData(mod, {});
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), 0u, 0u, bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, DirectUse_CalledByEntryPoint) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ b.Append(bar->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, buffer);
+ b.Return(bar, len);
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* len = b.Call<u32>(bar);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func():u32 {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ ret %3
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %5:u32 = call %bar
+ %let:u32 = let %5
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%bar = func(%tint_array_lengths:tint_array_lengths_struct):u32 {
+ $B2: {
+ %5:u32 = access %tint_array_lengths, 0u
+ ret %5
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %7:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %8:ptr<immediate, vec4<u32>, read> = access %7, 0u
+ %9:u32 = load_vector_element %8, 0u
+ %10:u32 = div %9, 4u
+ %11:tint_array_lengths_struct = construct %10
+ %12:u32 = call %bar, %11
+ %let:u32 = let %12
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaAccess_StructMember) {
+ auto* arr = ty.array<i32>();
+ auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+ {mod.symbols.New("a"), arr},
+ });
+ auto* arr_ptr = ty.ptr<storage>(arr);
+ auto* structure_ptr = ty.ptr<storage>(structure);
+
+ auto* buffer = b.Var("buffer", structure_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, b.Access(arr_ptr, buffer, 0_u));
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+MyStruct = struct @align(4) {
+ a:array<i32> @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, MyStruct, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:ptr<storage, array<i32>, read_write> = access %buffer, 0u
+ %4:u32 = arrayLength %3
+ %let:u32 = let %4
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+MyStruct = struct @align(4) {
+ a:array<i32> @offset(0)
+}
+
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, MyStruct, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %4:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %5:ptr<immediate, vec4<u32>, read> = access %4, 0u
+ %6:u32 = load_vector_element %5, 0u
+ %7:u32 = sub %6, 0u
+ %8:u32 = div %7, 4u
+ %9:tint_array_lengths_struct = construct %8
+ %10:ptr<storage, array<i32>, read_write> = access %buffer, 0u
+ %11:u32 = access %9, 0u
+ %let:u32 = let %11
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaAccess_StructMember_NonZeroOffset) {
+ auto* arr = ty.array<i32>();
+ auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+ {mod.symbols.New("u1"), ty.u32()},
+ {mod.symbols.New("u2"), ty.u32()},
+ {mod.symbols.New("u3"), ty.u32()},
+ {mod.symbols.New("u4"), ty.u32()},
+ {mod.symbols.New("u5"), ty.u32()},
+ {mod.symbols.New("a"), arr},
+ });
+ auto* arr_ptr = ty.ptr<storage>(arr);
+ auto* structure_ptr = ty.ptr<storage>(structure);
+
+ auto* buffer = b.Var("buffer", structure_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, b.Access(arr_ptr, buffer, 5_u));
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+MyStruct = struct @align(4) {
+ u1:u32 @offset(0)
+ u2:u32 @offset(4)
+ u3:u32 @offset(8)
+ u4:u32 @offset(12)
+ u5:u32 @offset(16)
+ a:array<i32> @offset(20)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, MyStruct, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:ptr<storage, array<i32>, read_write> = access %buffer, 5u
+ %4:u32 = arrayLength %3
+ %let:u32 = let %4
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+MyStruct = struct @align(4) {
+ u1:u32 @offset(0)
+ u2:u32 @offset(4)
+ u3:u32 @offset(8)
+ u4:u32 @offset(12)
+ u5:u32 @offset(16)
+ a:array<i32> @offset(20)
+}
+
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, MyStruct, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %4:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %5:ptr<immediate, vec4<u32>, read> = access %4, 0u
+ %6:u32 = load_vector_element %5, 0u
+ %7:u32 = sub %6, 20u
+ %8:u32 = div %7, 4u
+ %9:tint_array_lengths_struct = construct %8
+ %10:ptr<storage, array<i32>, read_write> = access %buffer, 5u
+ %11:u32 = access %9, 0u
+ %let:u32 = let %11
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaLet) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, b.Let("let", buffer));
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %let:ptr<storage, array<i32>, read_write> = let %buffer
+ %4:u32 = arrayLength %let
+ %let_1:u32 = let %4 # %let_1: 'let'
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %4:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %5:ptr<immediate, vec4<u32>, read> = access %4, 0u
+ %6:u32 = load_vector_element %5, 0u
+ %7:u32 = div %6, 4u
+ %8:tint_array_lengths_struct = construct %7
+ %let:ptr<storage, array<i32>, read_write> = let %buffer
+ %10:u32 = access %8, 0u
+ %let_1:u32 = let %10 # %let_1: 'let'
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaParameter) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ auto* param = b.FunctionParam("param", arr_ptr);
+ bar->SetParams({param});
+ b.Append(bar->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, param);
+ b.Return(bar, len);
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* len = b.Call<u32>(bar, buffer);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ %4:u32 = arrayLength %param
+ ret %4
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %6:u32 = call %bar, %buffer
+ %let:u32 = let %6
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>, %tint_array_length:u32):u32 {
+ $B2: {
+ ret %tint_array_length
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %7:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %8:ptr<immediate, vec4<u32>, read> = access %7, 0u
+ %9:u32 = load_vector_element %8, 0u
+ %10:u32 = div %9, 4u
+ %11:tint_array_lengths_struct = construct %10
+ %12:u32 = access %11, 0u
+ %13:u32 = call %bar, %buffer, %12
+ %let:u32 = let %13
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaParameterChain) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* zoo = b.Function("foo", ty.u32());
+ auto* param_zoo = b.FunctionParam("param_zoo", arr_ptr);
+ zoo->SetParams({param_zoo});
+ b.Append(zoo->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, param_zoo);
+ b.Return(zoo, len);
+ });
+
+ auto* bar = b.Function("foo", ty.u32());
+ auto* param_bar = b.FunctionParam("param_bar", arr_ptr);
+ bar->SetParams({param_bar});
+ b.Append(bar->Block(), [&] {
+ auto* len = b.Call<u32>(zoo, param_bar);
+ b.Return(bar, len);
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* len = b.Call<u32>(bar, buffer);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = func(%param_zoo:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ %4:u32 = arrayLength %param_zoo
+ ret %4
+ }
+}
+%foo_1 = func(%param_bar:ptr<storage, array<i32>, read_write>):u32 { # %foo_1: 'foo'
+ $B3: {
+ %7:u32 = call %foo, %param_bar
+ ret %7
+ }
+}
+%foo_2 = @compute @workgroup_size(1u, 1u, 1u) func():void { # %foo_2: 'foo'
+ $B4: {
+ %9:u32 = call %foo_1, %buffer
+ %let:u32 = let %9
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = func(%param_zoo:ptr<storage, array<i32>, read_write>, %tint_array_length:u32):u32 {
+ $B2: {
+ ret %tint_array_length
+ }
+}
+%foo_1 = func(%param_bar:ptr<storage, array<i32>, read_write>, %tint_array_length_1:u32):u32 { # %foo_1: 'foo', %tint_array_length_1: 'tint_array_length'
+ $B3: {
+ %9:u32 = call %foo, %param_bar, %tint_array_length_1
+ ret %9
+ }
+}
+%foo_2 = @compute @workgroup_size(1u, 1u, 1u) func():void { # %foo_2: 'foo'
+ $B4: {
+ %11:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %12:ptr<immediate, vec4<u32>, read> = access %11, 0u
+ %13:u32 = load_vector_element %12, 0u
+ %14:u32 = div %13, 4u
+ %15:tint_array_lengths_struct = construct %14
+ %16:u32 = access %15, 0u
+ %17:u32 = call %foo_1, %buffer, %16
+ %let:u32 = let %17
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+// Test that when we arrayLength is called on a parameter but the originating variable at the
+// callsite is not in the bindpoint map, we reintroduce an arrayLength call instead of passing
+// undef to the callee.
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaParameter_NotInMap) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ auto* param = b.FunctionParam("param", arr_ptr);
+ bar->SetParams({param});
+ b.Append(bar->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, param);
+ b.Return(bar, len);
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* len = b.Call<u32>(bar, buffer);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ %4:u32 = arrayLength %param
+ ret %4
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %6:u32 = call %bar, %buffer
+ %let:u32 = let %6
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>, %tint_array_length:u32):u32 {
+ $B2: {
+ ret %tint_array_length
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %6:u32 = arrayLength %buffer
+ %7:u32 = call %bar, %buffer, %6
+ %let:u32 = let %7
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ auto immediate_data = PrepareImmediateData(mod, {});
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), 0u, 0u, bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+// Test that we reuse the length parameter for multiple arrayLength calls on the same parameter.
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaParameter_MultipleCallsSameParameter) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ auto* param = b.FunctionParam("param", arr_ptr);
+ bar->SetParams({param});
+ b.Append(bar->Block(), [&] {
+ auto* len_a = b.Call<u32>(BuiltinFn::kArrayLength, param);
+ auto* len_b = b.Call<u32>(BuiltinFn::kArrayLength, param);
+ auto* len_c = b.Call<u32>(BuiltinFn::kArrayLength, param);
+ b.Return(bar, b.Add<u32>(len_a, b.Add<u32>(len_b, len_c)));
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* len = b.Call<u32>(bar, buffer);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ %4:u32 = arrayLength %param
+ %5:u32 = arrayLength %param
+ %6:u32 = arrayLength %param
+ %7:u32 = add %5, %6
+ %8:u32 = add %4, %7
+ ret %8
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %10:u32 = call %bar, %buffer
+ %let:u32 = let %10
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>, %tint_array_length:u32):u32 {
+ $B2: {
+ %6:u32 = add %tint_array_length, %tint_array_length
+ %7:u32 = add %tint_array_length, %6
+ ret %7
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %9:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %10:ptr<immediate, vec4<u32>, read> = access %9, 0u
+ %11:u32 = load_vector_element %10, 0u
+ %12:u32 = div %11, 4u
+ %13:tint_array_lengths_struct = construct %12
+ %14:u32 = access %13, 0u
+ %15:u32 = call %bar, %buffer, %14
+ %let:u32 = let %15
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaParameter_MultipleCallsDifferentParameters) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ auto* param_a = b.FunctionParam("param_a", arr_ptr);
+ auto* param_b = b.FunctionParam("param_b", arr_ptr);
+ auto* param_c = b.FunctionParam("param_c", arr_ptr);
+ bar->SetParams({param_a, param_b, param_c});
+ b.Append(bar->Block(), [&] {
+ auto* len_a = b.Call<u32>(BuiltinFn::kArrayLength, param_a);
+ auto* len_b = b.Call<u32>(BuiltinFn::kArrayLength, param_b);
+ auto* len_c = b.Call<u32>(BuiltinFn::kArrayLength, param_c);
+ b.Return(bar, b.Add<u32>(len_a, b.Add<u32>(len_b, len_c)));
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* len = b.Call<u32>(bar, buffer, buffer, buffer);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%param_a:ptr<storage, array<i32>, read_write>, %param_b:ptr<storage, array<i32>, read_write>, %param_c:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ %6:u32 = arrayLength %param_a
+ %7:u32 = arrayLength %param_b
+ %8:u32 = arrayLength %param_c
+ %9:u32 = add %7, %8
+ %10:u32 = add %6, %9
+ ret %10
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %12:u32 = call %bar, %buffer, %buffer, %buffer
+ %let:u32 = let %12
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%bar = func(%param_a:ptr<storage, array<i32>, read_write>, %param_b:ptr<storage, array<i32>, read_write>, %param_c:ptr<storage, array<i32>, read_write>, %tint_array_length:u32, %tint_array_length_1:u32, %tint_array_length_2:u32):u32 { # %tint_array_length_1: 'tint_array_length', %tint_array_length_2: 'tint_array_length'
+ $B2: {
+ %10:u32 = add %tint_array_length_1, %tint_array_length_2
+ %11:u32 = add %tint_array_length, %10
+ ret %11
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %13:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %14:ptr<immediate, vec4<u32>, read> = access %13, 0u
+ %15:u32 = load_vector_element %14, 0u
+ %16:u32 = div %15, 4u
+ %17:tint_array_lengths_struct = construct %16
+ %18:u32 = access %17, 0u
+ %19:u32 = access %17, 0u
+ %20:u32 = access %17, 0u
+ %21:u32 = call %bar, %buffer, %buffer, %buffer, %18, %19, %20
+ %let:u32 = let %21
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ViaComplexChain) {
+ auto* arr = ty.array<i32>();
+ auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+ {mod.symbols.New("u1"), ty.u32()},
+ {mod.symbols.New("a"), arr},
+ });
+ auto* arr_ptr = ty.ptr<storage>(arr);
+ auto* structure_ptr = ty.ptr<storage>(structure);
+
+ auto* buffer = b.Var("buffer", structure_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* bar = b.Function("bar", ty.u32());
+ auto* param = b.FunctionParam("param", arr_ptr);
+ bar->SetParams({param});
+ b.Append(bar->Block(), [&] {
+ auto* let1 = b.Let("let1", param);
+ auto* let2 = b.Let("let2", let1);
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, let2);
+ b.Return(bar, len);
+ });
+
+ auto* foo = b.ComputeFunction("foo");
+ b.Append(foo->Block(), [&] {
+ auto* access = b.Access(arr_ptr, buffer, 1_u);
+ auto* let = b.Let("let", access);
+ auto* len = b.Call<u32>(bar, let);
+ b.Let("let", len);
+ b.Return(foo);
+ });
+
+ auto* src = R"(
+MyStruct = struct @align(4) {
+ u1:u32 @offset(0)
+ a:array<i32> @offset(4)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, MyStruct, read_write> = var undef @binding_point(0, 0)
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>):u32 {
+ $B2: {
+ %let1:ptr<storage, array<i32>, read_write> = let %param
+ %let2:ptr<storage, array<i32>, read_write> = let %let1
+ %6:u32 = arrayLength %let2
+ ret %6
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %8:ptr<storage, array<i32>, read_write> = access %buffer, 1u
+ %let:ptr<storage, array<i32>, read_write> = let %8
+ %10:u32 = call %bar, %let
+ %let_1:u32 = let %10 # %let_1: 'let'
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+MyStruct = struct @align(4) {
+ u1:u32 @offset(0)
+ a:array<i32> @offset(4)
+}
+
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, MyStruct, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%bar = func(%param:ptr<storage, array<i32>, read_write>, %tint_array_length:u32):u32 {
+ $B2: {
+ %let1:ptr<storage, array<i32>, read_write> = let %param
+ %let2:ptr<storage, array<i32>, read_write> = let %let1
+ ret %tint_array_length
+ }
+}
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B3: {
+ %9:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %10:ptr<immediate, vec4<u32>, read> = access %9, 0u
+ %11:u32 = load_vector_element %10, 0u
+ %12:u32 = sub %11, 4u
+ %13:u32 = div %12, 4u
+ %14:tint_array_lengths_struct = construct %13
+ %15:ptr<storage, array<i32>, read_write> = access %buffer, 1u
+ %let:ptr<storage, array<i32>, read_write> = let %15
+ %17:u32 = access %14, 0u
+ %18:u32 = call %bar, %let, %17
+ %let_1:u32 = let %18 # %let_1: 'let'
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, ElementStrideLargerThanSize) {
+ auto* arr = ty.array<vec3<i32>>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer = b.Var("buffer", arr_ptr);
+ buffer->SetBindingPoint(0, 0);
+ mod.root_block->Append(buffer);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ auto* len = b.Call<u32>(BuiltinFn::kArrayLength, buffer);
+ b.Let("let", len);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer:ptr<storage, array<vec3<i32>>, read_write> = var undef @binding_point(0, 0)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %3:u32 = arrayLength %buffer
+ %let:u32 = let %3
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 1> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+}
+
+$B1: { # root
+ %buffer:ptr<storage, array<vec3<i32>>, read_write> = var undef @binding_point(0, 0)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %4:ptr<immediate, array<vec4<u32>, 1>, read> = access %tint_immediate_data, 0u
+ %5:ptr<immediate, vec4<u32>, read> = access %4, 0u
+ %6:u32 = load_vector_element %5, 0u
+ %7:u32 = div %6, 16u
+ %8:tint_array_lengths_struct = construct %7
+ %9:u32 = access %8, 0u
+ %let:u32 = let %9
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ArrayLengthFromImmediatesTest, MultipleVars) {
+ auto* arr = ty.array<i32>();
+ auto* arr_ptr = ty.ptr<storage>(arr);
+
+ auto* buffer_a = b.Var("buffer_a", arr_ptr);
+ auto* buffer_b = b.Var("buffer_b", arr_ptr);
+ auto* buffer_c = b.Var("buffer_c", arr_ptr);
+ auto* buffer_d = b.Var("buffer_d", arr_ptr);
+ auto* buffer_e = b.Var("buffer_e", arr_ptr);
+ buffer_a->SetBindingPoint(0, 0);
+ buffer_b->SetBindingPoint(0, 1);
+ buffer_c->SetBindingPoint(1, 0);
+ buffer_d->SetBindingPoint(1, 1);
+ buffer_e->SetBindingPoint(2, 3);
+ mod.root_block->Append(buffer_a);
+ mod.root_block->Append(buffer_b);
+ mod.root_block->Append(buffer_c);
+ mod.root_block->Append(buffer_d);
+ mod.root_block->Append(buffer_e);
+
+ auto* func = b.ComputeFunction("foo");
+ b.Append(func->Block(), [&] {
+ b.Call<u32>(BuiltinFn::kArrayLength, buffer_a);
+ b.Call<u32>(BuiltinFn::kArrayLength, buffer_b);
+ b.Call<u32>(BuiltinFn::kArrayLength, buffer_c);
+ b.Call<u32>(BuiltinFn::kArrayLength, buffer_d);
+ b.Call<u32>(BuiltinFn::kArrayLength, buffer_e);
+ b.Return(func);
+ });
+
+ auto* src = R"(
+$B1: { # root
+ %buffer_a:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %buffer_b:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 1)
+ %buffer_c:ptr<storage, array<i32>, read_write> = var undef @binding_point(1, 0)
+ %buffer_d:ptr<storage, array<i32>, read_write> = var undef @binding_point(1, 1)
+ %buffer_e:ptr<storage, array<i32>, read_write> = var undef @binding_point(2, 3)
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %7:u32 = arrayLength %buffer_a
+ %8:u32 = arrayLength %buffer_b
+ %9:u32 = arrayLength %buffer_c
+ %10:u32 = arrayLength %buffer_d
+ %11:u32 = arrayLength %buffer_e
+ ret
+ }
+}
+)";
+ EXPECT_EQ(src, str());
+
+ auto* expect = R"(
+tint_immediate_data_struct = struct @align(16), @block {
+ tint_storage_buffer_sizes:array<vec4<u32>, 2> @offset(16)
+}
+
+tint_array_lengths_struct = struct @align(4) {
+ tint_array_length_0_0:u32 @offset(0)
+ tint_array_length_0_1:u32 @offset(4)
+ tint_array_length_1_0:u32 @offset(8)
+ tint_array_length_1_1:u32 @offset(12)
+ tint_array_length_2_3:u32 @offset(16)
+}
+
+$B1: { # root
+ %buffer_a:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 0)
+ %buffer_b:ptr<storage, array<i32>, read_write> = var undef @binding_point(0, 1)
+ %buffer_c:ptr<storage, array<i32>, read_write> = var undef @binding_point(1, 0)
+ %buffer_d:ptr<storage, array<i32>, read_write> = var undef @binding_point(1, 1)
+ %buffer_e:ptr<storage, array<i32>, read_write> = var undef @binding_point(2, 3)
+ %tint_immediate_data:ptr<immediate, tint_immediate_data_struct, read> = var undef
+}
+
+%foo = @compute @workgroup_size(1u, 1u, 1u) func():void {
+ $B2: {
+ %8:ptr<immediate, array<vec4<u32>, 2>, read> = access %tint_immediate_data, 0u
+ %9:ptr<immediate, vec4<u32>, read> = access %8, 0u
+ %10:u32 = load_vector_element %9, 0u
+ %11:u32 = div %10, 4u
+ %12:ptr<immediate, array<vec4<u32>, 2>, read> = access %tint_immediate_data, 0u
+ %13:ptr<immediate, vec4<u32>, read> = access %12, 1u
+ %14:u32 = load_vector_element %13, 1u
+ %15:u32 = div %14, 4u
+ %16:ptr<immediate, array<vec4<u32>, 2>, read> = access %tint_immediate_data, 0u
+ %17:ptr<immediate, vec4<u32>, read> = access %16, 0u
+ %18:u32 = load_vector_element %17, 3u
+ %19:u32 = div %18, 4u
+ %20:ptr<immediate, array<vec4<u32>, 2>, read> = access %tint_immediate_data, 0u
+ %21:ptr<immediate, vec4<u32>, read> = access %20, 0u
+ %22:u32 = load_vector_element %21, 2u
+ %23:u32 = div %22, 4u
+ %24:ptr<immediate, array<vec4<u32>, 2>, read> = access %tint_immediate_data, 0u
+ %25:ptr<immediate, vec4<u32>, read> = access %24, 1u
+ %26:u32 = load_vector_element %25, 0u
+ %27:u32 = div %26, 4u
+ %28:tint_array_lengths_struct = construct %11, %15, %19, %23, %27
+ %29:u32 = access %28, 0u
+ %30:u32 = access %28, 1u
+ %31:u32 = access %28, 2u
+ %32:u32 = access %28, 3u
+ %33:u32 = access %28, 4u
+ ret
+ }
+}
+)";
+
+ std::unordered_map<BindingPoint, uint32_t> bindpoint_to_index;
+ bindpoint_to_index[{0, 0}] = 0;
+ bindpoint_to_index[{0, 1}] = 5;
+ bindpoint_to_index[{1, 0}] = 3;
+ bindpoint_to_index[{1, 1}] = 2;
+ bindpoint_to_index[{2, 3}] = 4;
+
+ core::ir::transform::PrepareImmediateDataConfig immediate_data_config;
+ constexpr uint32_t buffer_size_start_offset = 16;
+ uint32_t num_elements = GetBufferSizesNumElements(bindpoint_to_index);
+ immediate_data_config.AddInternalImmediateData(buffer_size_start_offset,
+ mod.symbols.New("tint_storage_buffer_sizes"),
+ ty.array(ty.vec4<u32>(), num_elements));
+ auto immediate_data = PrepareImmediateData(mod, immediate_data_config);
+ EXPECT_EQ(immediate_data, Success);
+ Run(ArrayLengthFromImmediates, immediate_data.Get(), buffer_size_start_offset, num_elements,
+ bindpoint_to_index);
+
+ EXPECT_EQ(expect, str());
+}
+
+} // namespace
+} // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/prepare_immediate_data.h b/src/tint/lang/core/ir/transform/prepare_immediate_data.h
index 6d26694..29c2253 100644
--- a/src/tint/lang/core/ir/transform/prepare_immediate_data.h
+++ b/src/tint/lang/core/ir/transform/prepare_immediate_data.h
@@ -58,7 +58,7 @@
core::ir::Var* var = nullptr;
/// A map from member offset to member index.
- Hashmap<uint32_t, uint32_t, 4> offset_to_index;
+ Hashmap<uint32_t, uint32_t, 6> offset_to_index;
/// @returns the member index of the constant at @p offset
uint32_t IndexOf(uint32_t offset) const {