| // Copyright 2021 The Tint Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "src/tint/transform/decompose_memory_access.h" |
| |
| #include <memory> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "src/tint/ast/assignment_statement.h" |
| #include "src/tint/ast/call_statement.h" |
| #include "src/tint/ast/disable_validation_attribute.h" |
| #include "src/tint/ast/type_name.h" |
| #include "src/tint/ast/unary_op.h" |
| #include "src/tint/program_builder.h" |
| #include "src/tint/sem/call.h" |
| #include "src/tint/sem/member_accessor_expression.h" |
| #include "src/tint/sem/statement.h" |
| #include "src/tint/sem/struct.h" |
| #include "src/tint/sem/variable.h" |
| #include "src/tint/type/array.h" |
| #include "src/tint/type/atomic.h" |
| #include "src/tint/type/reference.h" |
| #include "src/tint/utils/block_allocator.h" |
| #include "src/tint/utils/hash.h" |
| #include "src/tint/utils/map.h" |
| |
| using namespace tint::number_suffixes; // NOLINT |
| |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::DecomposeMemoryAccess); |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::DecomposeMemoryAccess::Intrinsic); |
| |
| namespace tint::transform { |
| |
| namespace { |
| |
| bool ShouldRun(const Program* program) { |
| for (auto* decl : program->AST().GlobalDeclarations()) { |
| if (auto* var = program->Sem().Get<sem::Variable>(decl)) { |
| if (var->AddressSpace() == ast::AddressSpace::kStorage || |
| var->AddressSpace() == ast::AddressSpace::kUniform) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /// Offset is a simple ast::Expression builder interface, used to build byte |
| /// offsets for storage and uniform buffer accesses. |
| struct Offset : Castable<Offset> { |
| /// @returns builds and returns the ast::Expression in `ctx.dst` |
| virtual const ast::Expression* Build(CloneContext& ctx) const = 0; |
| }; |
| |
| /// OffsetExpr is an implementation of Offset that clones and casts the given |
| /// expression to `u32`. |
| struct OffsetExpr : Offset { |
| const ast::Expression* const expr = nullptr; |
| |
| explicit OffsetExpr(const ast::Expression* e) : expr(e) {} |
| |
| const ast::Expression* Build(CloneContext& ctx) const override { |
| auto* type = ctx.src->Sem().Get(expr)->Type()->UnwrapRef(); |
| auto* res = ctx.Clone(expr); |
| if (!type->Is<type::U32>()) { |
| res = ctx.dst->Construct<u32>(res); |
| } |
| return res; |
| } |
| }; |
| |
| /// OffsetLiteral is an implementation of Offset that constructs a u32 literal |
| /// value. |
| struct OffsetLiteral final : Castable<OffsetLiteral, Offset> { |
| uint32_t const literal = 0; |
| |
| explicit OffsetLiteral(uint32_t lit) : literal(lit) {} |
| |
| const ast::Expression* Build(CloneContext& ctx) const override { |
| return ctx.dst->Expr(u32(literal)); |
| } |
| }; |
| |
| /// OffsetBinOp is an implementation of Offset that constructs a binary-op of |
| /// two Offsets. |
| struct OffsetBinOp : Offset { |
| ast::BinaryOp op; |
| Offset const* lhs = nullptr; |
| Offset const* rhs = nullptr; |
| |
| const ast::Expression* Build(CloneContext& ctx) const override { |
| return ctx.dst->create<ast::BinaryExpression>(op, lhs->Build(ctx), rhs->Build(ctx)); |
| } |
| }; |
| |
| /// LoadStoreKey is the unordered map key to a load or store intrinsic. |
| struct LoadStoreKey { |
| ast::AddressSpace const address_space; // buffer address space |
| ast::Access const access; // buffer access |
| type::Type const* buf_ty = nullptr; // buffer type |
| type::Type const* el_ty = nullptr; // element type |
| bool operator==(const LoadStoreKey& rhs) const { |
| return address_space == rhs.address_space && access == rhs.access && buf_ty == rhs.buf_ty && |
| el_ty == rhs.el_ty; |
| } |
| struct Hasher { |
| inline std::size_t operator()(const LoadStoreKey& u) const { |
| return utils::Hash(u.address_space, u.access, u.buf_ty, u.el_ty); |
| } |
| }; |
| }; |
| |
| /// AtomicKey is the unordered map key to an atomic intrinsic. |
| struct AtomicKey { |
| ast::Access const access; // buffer access |
| type::Type const* buf_ty = nullptr; // buffer type |
| type::Type const* el_ty = nullptr; // element type |
| sem::BuiltinType const op; // atomic op |
| bool operator==(const AtomicKey& rhs) const { |
| return access == rhs.access && buf_ty == rhs.buf_ty && el_ty == rhs.el_ty && op == rhs.op; |
| } |
| struct Hasher { |
| inline std::size_t operator()(const AtomicKey& u) const { |
| return utils::Hash(u.access, u.buf_ty, u.el_ty, u.op); |
| } |
| }; |
| }; |
| |
| bool IntrinsicDataTypeFor(const type::Type* ty, DecomposeMemoryAccess::Intrinsic::DataType& out) { |
| if (ty->Is<type::I32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kI32; |
| return true; |
| } |
| if (ty->Is<type::U32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kU32; |
| return true; |
| } |
| if (ty->Is<type::F32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kF32; |
| return true; |
| } |
| if (ty->Is<type::F16>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kF16; |
| return true; |
| } |
| if (auto* vec = ty->As<type::Vector>()) { |
| switch (vec->Width()) { |
| case 2: |
| if (vec->type()->Is<type::I32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2I32; |
| return true; |
| } |
| if (vec->type()->Is<type::U32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2U32; |
| return true; |
| } |
| if (vec->type()->Is<type::F32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2F32; |
| return true; |
| } |
| if (vec->type()->Is<type::F16>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2F16; |
| return true; |
| } |
| break; |
| case 3: |
| if (vec->type()->Is<type::I32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3I32; |
| return true; |
| } |
| if (vec->type()->Is<type::U32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3U32; |
| return true; |
| } |
| if (vec->type()->Is<type::F32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3F32; |
| return true; |
| } |
| if (vec->type()->Is<type::F16>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3F16; |
| return true; |
| } |
| break; |
| case 4: |
| if (vec->type()->Is<type::I32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4I32; |
| return true; |
| } |
| if (vec->type()->Is<type::U32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4U32; |
| return true; |
| } |
| if (vec->type()->Is<type::F32>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4F32; |
| return true; |
| } |
| if (vec->type()->Is<type::F16>()) { |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4F16; |
| return true; |
| } |
| break; |
| } |
| return false; |
| } |
| |
| return false; |
| } |
| |
| /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied |
| /// to a stub function to load the type `ty`. |
| DecomposeMemoryAccess::Intrinsic* IntrinsicLoadFor(ProgramBuilder* builder, |
| ast::AddressSpace address_space, |
| const type::Type* ty) { |
| DecomposeMemoryAccess::Intrinsic::DataType type; |
| if (!IntrinsicDataTypeFor(ty, type)) { |
| return nullptr; |
| } |
| return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( |
| builder->ID(), builder->AllocateNodeID(), DecomposeMemoryAccess::Intrinsic::Op::kLoad, |
| address_space, type); |
| } |
| |
| /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied |
| /// to a stub function to store the type `ty`. |
| DecomposeMemoryAccess::Intrinsic* IntrinsicStoreFor(ProgramBuilder* builder, |
| ast::AddressSpace address_space, |
| const type::Type* ty) { |
| DecomposeMemoryAccess::Intrinsic::DataType type; |
| if (!IntrinsicDataTypeFor(ty, type)) { |
| return nullptr; |
| } |
| return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( |
| builder->ID(), builder->AllocateNodeID(), DecomposeMemoryAccess::Intrinsic::Op::kStore, |
| address_space, type); |
| } |
| |
| /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied |
| /// to a stub function for the atomic op and the type `ty`. |
| DecomposeMemoryAccess::Intrinsic* IntrinsicAtomicFor(ProgramBuilder* builder, |
| sem::BuiltinType ity, |
| const type::Type* ty) { |
| auto op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicLoad; |
| switch (ity) { |
| case sem::BuiltinType::kAtomicLoad: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicLoad; |
| break; |
| case sem::BuiltinType::kAtomicStore: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicStore; |
| break; |
| case sem::BuiltinType::kAtomicAdd: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicAdd; |
| break; |
| case sem::BuiltinType::kAtomicSub: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicSub; |
| break; |
| case sem::BuiltinType::kAtomicMax: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicMax; |
| break; |
| case sem::BuiltinType::kAtomicMin: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicMin; |
| break; |
| case sem::BuiltinType::kAtomicAnd: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicAnd; |
| break; |
| case sem::BuiltinType::kAtomicOr: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicOr; |
| break; |
| case sem::BuiltinType::kAtomicXor: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicXor; |
| break; |
| case sem::BuiltinType::kAtomicExchange: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicExchange; |
| break; |
| case sem::BuiltinType::kAtomicCompareExchangeWeak: |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicCompareExchangeWeak; |
| break; |
| default: |
| TINT_ICE(Transform, builder->Diagnostics()) |
| << "invalid IntrinsicType for DecomposeMemoryAccess::Intrinsic: " |
| << ty->TypeInfo().name; |
| break; |
| } |
| |
| DecomposeMemoryAccess::Intrinsic::DataType type; |
| if (!IntrinsicDataTypeFor(ty, type)) { |
| return nullptr; |
| } |
| return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( |
| builder->ID(), builder->AllocateNodeID(), op, ast::AddressSpace::kStorage, type); |
| } |
| |
| /// BufferAccess describes a single storage or uniform buffer access |
| struct BufferAccess { |
| sem::Expression const* var = nullptr; // Storage buffer variable |
| Offset const* offset = nullptr; // The byte offset on var |
| type::Type const* type = nullptr; // The type of the access |
| operator bool() const { return var; } // Returns true if valid |
| }; |
| |
| /// Store describes a single storage or uniform buffer write |
| struct Store { |
| const ast::AssignmentStatement* assignment; // The AST assignment statement |
| BufferAccess target; // The target for the write |
| }; |
| |
| } // namespace |
| |
| /// PIMPL state for the transform |
| struct DecomposeMemoryAccess::State { |
| /// The clone context |
| CloneContext& ctx; |
| /// Alias to `*ctx.dst` |
| ProgramBuilder& b; |
| /// Map of AST expression to storage or uniform buffer access |
| /// This map has entries added when encountered, and removed when outer |
| /// expressions chain the access. |
| /// Subset of #expression_order, as expressions are not removed from |
| /// #expression_order. |
| std::unordered_map<const ast::Expression*, BufferAccess> accesses; |
| /// The visited order of AST expressions (superset of #accesses) |
| std::vector<const ast::Expression*> expression_order; |
| /// [buffer-type, element-type] -> load function name |
| std::unordered_map<LoadStoreKey, Symbol, LoadStoreKey::Hasher> load_funcs; |
| /// [buffer-type, element-type] -> store function name |
| std::unordered_map<LoadStoreKey, Symbol, LoadStoreKey::Hasher> store_funcs; |
| /// [buffer-type, element-type, atomic-op] -> load function name |
| std::unordered_map<AtomicKey, Symbol, AtomicKey::Hasher> atomic_funcs; |
| /// List of storage or uniform buffer writes |
| std::vector<Store> stores; |
| /// Allocations for offsets |
| utils::BlockAllocator<Offset> offsets_; |
| |
| /// Constructor |
| /// @param context the CloneContext |
| explicit State(CloneContext& context) : ctx(context), b(*ctx.dst) {} |
| |
| /// @param offset the offset value to wrap in an Offset |
| /// @returns an Offset for the given literal value |
| const Offset* ToOffset(uint32_t offset) { return offsets_.Create<OffsetLiteral>(offset); } |
| |
| /// @param expr the expression to convert to an Offset |
| /// @returns an Offset for the given ast::Expression |
| const Offset* ToOffset(const ast::Expression* expr) { |
| if (auto* lit = expr->As<ast::IntLiteralExpression>()) { |
| if (lit->value >= 0) { |
| return offsets_.Create<OffsetLiteral>(static_cast<uint32_t>(lit->value)); |
| } |
| } |
| return offsets_.Create<OffsetExpr>(expr); |
| } |
| |
| /// @param offset the Offset that is returned |
| /// @returns the given offset (pass-through) |
| const Offset* ToOffset(const Offset* offset) { return offset; } |
| |
| /// @param lhs_ the left-hand side of the add expression |
| /// @param rhs_ the right-hand side of the add expression |
| /// @return an Offset that is a sum of lhs and rhs, performing basic constant |
| /// folding if possible |
| template <typename LHS, typename RHS> |
| const Offset* Add(LHS&& lhs_, RHS&& rhs_) { |
| auto* lhs = ToOffset(std::forward<LHS>(lhs_)); |
| auto* rhs = ToOffset(std::forward<RHS>(rhs_)); |
| auto* lhs_lit = tint::As<OffsetLiteral>(lhs); |
| auto* rhs_lit = tint::As<OffsetLiteral>(rhs); |
| if (lhs_lit && lhs_lit->literal == 0) { |
| return rhs; |
| } |
| if (rhs_lit && rhs_lit->literal == 0) { |
| return lhs; |
| } |
| if (lhs_lit && rhs_lit) { |
| if (static_cast<uint64_t>(lhs_lit->literal) + static_cast<uint64_t>(rhs_lit->literal) <= |
| 0xffffffff) { |
| return offsets_.Create<OffsetLiteral>(lhs_lit->literal + rhs_lit->literal); |
| } |
| } |
| auto* out = offsets_.Create<OffsetBinOp>(); |
| out->op = ast::BinaryOp::kAdd; |
| out->lhs = lhs; |
| out->rhs = rhs; |
| return out; |
| } |
| |
| /// @param lhs_ the left-hand side of the multiply expression |
| /// @param rhs_ the right-hand side of the multiply expression |
| /// @return an Offset that is the multiplication of lhs and rhs, performing |
| /// basic constant folding if possible |
| template <typename LHS, typename RHS> |
| const Offset* Mul(LHS&& lhs_, RHS&& rhs_) { |
| auto* lhs = ToOffset(std::forward<LHS>(lhs_)); |
| auto* rhs = ToOffset(std::forward<RHS>(rhs_)); |
| auto* lhs_lit = tint::As<OffsetLiteral>(lhs); |
| auto* rhs_lit = tint::As<OffsetLiteral>(rhs); |
| if (lhs_lit && lhs_lit->literal == 0) { |
| return offsets_.Create<OffsetLiteral>(0u); |
| } |
| if (rhs_lit && rhs_lit->literal == 0) { |
| return offsets_.Create<OffsetLiteral>(0u); |
| } |
| if (lhs_lit && lhs_lit->literal == 1) { |
| return rhs; |
| } |
| if (rhs_lit && rhs_lit->literal == 1) { |
| return lhs; |
| } |
| if (lhs_lit && rhs_lit) { |
| return offsets_.Create<OffsetLiteral>(lhs_lit->literal * rhs_lit->literal); |
| } |
| auto* out = offsets_.Create<OffsetBinOp>(); |
| out->op = ast::BinaryOp::kMultiply; |
| out->lhs = lhs; |
| out->rhs = rhs; |
| return out; |
| } |
| |
| /// AddAccess() adds the `expr -> access` map item to #accesses, and `expr` |
| /// to #expression_order. |
| /// @param expr the expression that performs the access |
| /// @param access the access |
| void AddAccess(const ast::Expression* expr, const BufferAccess& access) { |
| TINT_ASSERT(Transform, access.type); |
| accesses.emplace(expr, access); |
| expression_order.emplace_back(expr); |
| } |
| |
| /// TakeAccess() removes the `node` item from #accesses (if it exists), |
| /// returning the BufferAccess. If #accesses does not hold an item for |
| /// `node`, an invalid BufferAccess is returned. |
| /// @param node the expression that performed an access |
| /// @return the BufferAccess for the given expression |
| BufferAccess TakeAccess(const ast::Expression* node) { |
| auto lhs_it = accesses.find(node); |
| if (lhs_it == accesses.end()) { |
| return {}; |
| } |
| auto access = lhs_it->second; |
| accesses.erase(node); |
| return access; |
| } |
| |
| /// LoadFunc() returns a symbol to an intrinsic function that loads an element of type `el_ty` |
| /// from a storage or uniform buffer of type `buf_ty`. |
| /// The emitted function has the signature: |
| /// `fn load(buf : ptr<SC, buf_ty, A>, offset : u32) -> el_ty` |
| /// @param buf_ty the storage or uniform buffer type |
| /// @param el_ty the storage or uniform buffer element type |
| /// @param var_user the variable user |
| /// @return the name of the function that performs the load |
| Symbol LoadFunc(const type::Type* buf_ty, |
| const type::Type* el_ty, |
| const sem::VariableUser* var_user) { |
| auto address_space = var_user->Variable()->AddressSpace(); |
| auto access = var_user->Variable()->Access(); |
| if (address_space != ast::AddressSpace::kStorage) { |
| access = ast::Access::kUndefined; |
| } |
| return utils::GetOrCreate( |
| load_funcs, LoadStoreKey{address_space, access, buf_ty, el_ty}, [&] { |
| utils::Vector params{ |
| b.Param("buffer", |
| b.ty.pointer(CreateASTTypeFor(ctx, buf_ty), address_space, access), |
| utils::Vector{b.Disable(ast::DisabledValidation::kFunctionParameter)}), |
| b.Param("offset", b.ty.u32()), |
| }; |
| |
| auto name = b.Sym(); |
| |
| if (auto* intrinsic = IntrinsicLoadFor(ctx.dst, address_space, el_ty)) { |
| auto* el_ast_ty = CreateASTTypeFor(ctx, el_ty); |
| auto* func = b.create<ast::Function>( |
| name, params, el_ast_ty, nullptr, |
| utils::Vector{ |
| intrinsic, |
| b.Disable(ast::DisabledValidation::kFunctionHasNoBody), |
| }, |
| utils::Empty); |
| b.AST().AddFunction(func); |
| } else if (auto* arr_ty = el_ty->As<type::Array>()) { |
| // fn load_func(buffer : buf_ty, offset : u32) -> array<T, N> { |
| // var arr : array<T, N>; |
| // for (var i = 0u; i < array_count; i = i + 1) { |
| // arr[i] = el_load_func(buffer, offset + i * array_stride) |
| // } |
| // return arr; |
| // } |
| auto load = LoadFunc(buf_ty, arr_ty->ElemType()->UnwrapRef(), var_user); |
| auto* arr = b.Var(b.Symbols().New("arr"), CreateASTTypeFor(ctx, arr_ty)); |
| auto* i = b.Var(b.Symbols().New("i"), b.Expr(0_u)); |
| auto* for_init = b.Decl(i); |
| auto arr_cnt = arr_ty->ConstantCount(); |
| if (!arr_cnt) { |
| // Non-constant counts should not be possible: |
| // * Override-expression counts can only be applied to workgroup arrays, and |
| // this method only handles storage and uniform. |
| // * Runtime-sized arrays are not loadable. |
| TINT_ICE(Transform, b.Diagnostics()) |
| << "unexpected non-constant array count"; |
| arr_cnt = 1; |
| } |
| auto* for_cond = b.create<ast::BinaryExpression>( |
| ast::BinaryOp::kLessThan, b.Expr(i), b.Expr(u32(arr_cnt.value()))); |
| auto* for_cont = b.Assign(i, b.Add(i, 1_u)); |
| auto* arr_el = b.IndexAccessor(arr, i); |
| auto* el_offset = b.Add(b.Expr("offset"), b.Mul(i, u32(arr_ty->Stride()))); |
| auto* el_val = b.Call(load, "buffer", el_offset); |
| auto* for_loop = |
| b.For(for_init, for_cond, for_cont, b.Block(b.Assign(arr_el, el_val))); |
| |
| b.Func(name, params, CreateASTTypeFor(ctx, arr_ty), |
| utils::Vector{ |
| b.Decl(arr), |
| for_loop, |
| b.Return(arr), |
| }); |
| } else { |
| utils::Vector<const ast::Expression*, 8> values; |
| if (auto* mat_ty = el_ty->As<type::Matrix>()) { |
| auto* vec_ty = mat_ty->ColumnType(); |
| Symbol load = LoadFunc(buf_ty, vec_ty, var_user); |
| for (uint32_t i = 0; i < mat_ty->columns(); i++) { |
| auto* offset = b.Add("offset", u32(i * mat_ty->ColumnStride())); |
| values.Push(b.Call(load, "buffer", offset)); |
| } |
| } else if (auto* str = el_ty->As<sem::Struct>()) { |
| for (auto* member : str->Members()) { |
| auto* offset = b.Add("offset", u32(member->Offset())); |
| Symbol load = LoadFunc(buf_ty, member->Type()->UnwrapRef(), var_user); |
| values.Push(b.Call(load, "buffer", offset)); |
| } |
| } |
| b.Func(name, params, CreateASTTypeFor(ctx, el_ty), |
| utils::Vector{ |
| b.Return(b.Construct(CreateASTTypeFor(ctx, el_ty), values)), |
| }); |
| } |
| return name; |
| }); |
| } |
| |
| /// StoreFunc() returns a symbol to an intrinsic function that stores an |
| /// element of type `el_ty` to a storage buffer of type `buf_ty`. |
| /// The function has the signature: |
| /// `fn store(buf : ptr<SC, buf_ty, A>, offset : u32, value : el_ty)` |
| /// @param buf_ty the storage buffer type |
| /// @param el_ty the storage buffer element type |
| /// @param var_user the variable user |
| /// @return the name of the function that performs the store |
| Symbol StoreFunc(const type::Type* buf_ty, |
| const type::Type* el_ty, |
| const sem::VariableUser* var_user) { |
| auto address_space = var_user->Variable()->AddressSpace(); |
| auto access = var_user->Variable()->Access(); |
| if (address_space != ast::AddressSpace::kStorage) { |
| access = ast::Access::kUndefined; |
| } |
| return utils::GetOrCreate( |
| store_funcs, LoadStoreKey{address_space, access, buf_ty, el_ty}, [&] { |
| utils::Vector params{ |
| b.Param("buffer", |
| b.ty.pointer(CreateASTTypeFor(ctx, buf_ty), address_space, access), |
| utils::Vector{b.Disable(ast::DisabledValidation::kFunctionParameter)}), |
| b.Param("offset", b.ty.u32()), |
| b.Param("value", CreateASTTypeFor(ctx, el_ty)), |
| }; |
| |
| auto name = b.Sym(); |
| |
| if (auto* intrinsic = IntrinsicStoreFor(ctx.dst, address_space, el_ty)) { |
| auto* func = b.create<ast::Function>( |
| name, params, b.ty.void_(), nullptr, |
| utils::Vector{ |
| intrinsic, |
| b.Disable(ast::DisabledValidation::kFunctionHasNoBody), |
| }, |
| utils::Empty); |
| b.AST().AddFunction(func); |
| } else { |
| auto body = Switch<utils::Vector<const ast::Statement*, 8>>( |
| el_ty, // |
| [&](const type::Array* arr_ty) { |
| // fn store_func(buffer : buf_ty, offset : u32, value : el_ty) { |
| // var array = value; // No dynamic indexing on constant arrays |
| // for (var i = 0u; i < array_count; i = i + 1) { |
| // arr[i] = el_store_func(buffer, offset + i * array_stride, |
| // value[i]) |
| // } |
| // return arr; |
| // } |
| auto* array = b.Var(b.Symbols().New("array"), b.Expr("value")); |
| auto store = |
| StoreFunc(buf_ty, arr_ty->ElemType()->UnwrapRef(), var_user); |
| auto* i = b.Var(b.Symbols().New("i"), b.Expr(0_u)); |
| auto* for_init = b.Decl(i); |
| auto arr_cnt = arr_ty->ConstantCount(); |
| if (!arr_cnt) { |
| // Non-constant counts should not be possible: |
| // * Override-expression counts can only be applied to workgroup |
| // arrays, and this method only handles storage and uniform. |
| // * Runtime-sized arrays are not storable. |
| TINT_ICE(Transform, b.Diagnostics()) |
| << "unexpected non-constant array count"; |
| arr_cnt = 1; |
| } |
| auto* for_cond = b.create<ast::BinaryExpression>( |
| ast::BinaryOp::kLessThan, b.Expr(i), b.Expr(u32(arr_cnt.value()))); |
| auto* for_cont = b.Assign(i, b.Add(i, 1_u)); |
| auto* arr_el = b.IndexAccessor(array, i); |
| auto* el_offset = |
| b.Add(b.Expr("offset"), b.Mul(i, u32(arr_ty->Stride()))); |
| auto* store_stmt = |
| b.CallStmt(b.Call(store, "buffer", el_offset, arr_el)); |
| auto* for_loop = |
| b.For(for_init, for_cond, for_cont, b.Block(store_stmt)); |
| |
| return utils::Vector{b.Decl(array), for_loop}; |
| }, |
| [&](const type::Matrix* mat_ty) { |
| auto* vec_ty = mat_ty->ColumnType(); |
| Symbol store = StoreFunc(buf_ty, vec_ty, var_user); |
| utils::Vector<const ast::Statement*, 4> stmts; |
| for (uint32_t i = 0; i < mat_ty->columns(); i++) { |
| auto* offset = b.Add("offset", u32(i * mat_ty->ColumnStride())); |
| auto* element = b.IndexAccessor("value", u32(i)); |
| auto* call = b.Call(store, "buffer", offset, element); |
| stmts.Push(b.CallStmt(call)); |
| } |
| return stmts; |
| }, |
| [&](const sem::Struct* str) { |
| utils::Vector<const ast::Statement*, 8> stmts; |
| for (auto* member : str->Members()) { |
| auto* offset = b.Add("offset", u32(member->Offset())); |
| auto* element = |
| b.MemberAccessor("value", ctx.Clone(member->Name())); |
| Symbol store = |
| StoreFunc(buf_ty, member->Type()->UnwrapRef(), var_user); |
| auto* call = b.Call(store, "buffer", offset, element); |
| stmts.Push(b.CallStmt(call)); |
| } |
| return stmts; |
| }); |
| |
| b.Func(name, params, b.ty.void_(), body); |
| } |
| |
| return name; |
| }); |
| } |
| |
| /// AtomicFunc() returns a symbol to an intrinsic function that performs an |
| /// atomic operation from a storage buffer of type `buf_ty`. The function has |
| /// the signature: |
| // `fn atomic_op(buf : ptr<storage, buf_ty, A>, offset : u32, ...) -> T` |
| /// @param buf_ty the storage buffer type |
| /// @param el_ty the storage buffer element type |
| /// @param intrinsic the atomic intrinsic |
| /// @param var_user the variable user |
| /// @return the name of the function that performs the load |
| Symbol AtomicFunc(const type::Type* buf_ty, |
| const type::Type* el_ty, |
| const sem::Builtin* intrinsic, |
| const sem::VariableUser* var_user) { |
| auto op = intrinsic->Type(); |
| auto address_space = var_user->Variable()->AddressSpace(); |
| auto access = var_user->Variable()->Access(); |
| if (address_space != ast::AddressSpace::kStorage) { |
| access = ast::Access::kUndefined; |
| } |
| return utils::GetOrCreate(atomic_funcs, AtomicKey{access, buf_ty, el_ty, op}, [&] { |
| // The first parameter to all WGSL atomics is the expression to the |
| // atomic. This is replaced with two parameters: the buffer and offset. |
| utils::Vector params{ |
| b.Param("buffer", |
| b.ty.pointer(CreateASTTypeFor(ctx, buf_ty), ast::AddressSpace::kStorage, |
| access), |
| utils::Vector{b.Disable(ast::DisabledValidation::kFunctionParameter)}), |
| b.Param("offset", b.ty.u32()), |
| }; |
| |
| // Other parameters are copied as-is: |
| for (size_t i = 1; i < intrinsic->Parameters().Length(); i++) { |
| auto* param = intrinsic->Parameters()[i]; |
| auto* ty = CreateASTTypeFor(ctx, param->Type()); |
| params.Push(b.Param("param_" + std::to_string(i), ty)); |
| } |
| |
| auto* atomic = IntrinsicAtomicFor(ctx.dst, op, el_ty); |
| if (atomic == nullptr) { |
| TINT_ICE(Transform, b.Diagnostics()) |
| << "IntrinsicAtomicFor() returned nullptr for op " << op << " and type " |
| << el_ty->TypeInfo().name; |
| } |
| |
| const ast::Type* ret_ty = nullptr; |
| |
| // For intrinsics that return a struct, there is no AST node for it, so create one now. |
| if (intrinsic->Type() == sem::BuiltinType::kAtomicCompareExchangeWeak) { |
| auto* str = intrinsic->ReturnType()->As<sem::Struct>(); |
| TINT_ASSERT(Transform, str && str->Declaration() == nullptr); |
| |
| utils::Vector<const ast::StructMember*, 8> ast_members; |
| ast_members.Reserve(str->Members().Length()); |
| for (auto& m : str->Members()) { |
| ast_members.Push( |
| b.Member(ctx.Clone(m->Name()), CreateASTTypeFor(ctx, m->Type()))); |
| } |
| |
| auto name = b.Symbols().New("atomic_compare_exchange_weak_ret_type"); |
| auto* new_str = b.Structure(name, std::move(ast_members)); |
| ret_ty = b.ty.Of(new_str); |
| } else { |
| ret_ty = CreateASTTypeFor(ctx, intrinsic->ReturnType()); |
| } |
| |
| auto* func = b.create<ast::Function>( |
| b.Symbols().New(std::string{"tint_"} + intrinsic->str()), params, ret_ty, nullptr, |
| utils::Vector{ |
| atomic, |
| b.Disable(ast::DisabledValidation::kFunctionHasNoBody), |
| }, |
| utils::Empty); |
| |
| b.AST().AddFunction(func); |
| return func->symbol; |
| }); |
| } |
| }; |
| |
| DecomposeMemoryAccess::Intrinsic::Intrinsic(ProgramID pid, |
| ast::NodeID nid, |
| Op o, |
| ast::AddressSpace sc, |
| DataType ty) |
| : Base(pid, nid), op(o), address_space(sc), type(ty) {} |
| DecomposeMemoryAccess::Intrinsic::~Intrinsic() = default; |
| std::string DecomposeMemoryAccess::Intrinsic::InternalName() const { |
| std::stringstream ss; |
| switch (op) { |
| case Op::kLoad: |
| ss << "intrinsic_load_"; |
| break; |
| case Op::kStore: |
| ss << "intrinsic_store_"; |
| break; |
| case Op::kAtomicLoad: |
| ss << "intrinsic_atomic_load_"; |
| break; |
| case Op::kAtomicStore: |
| ss << "intrinsic_atomic_store_"; |
| break; |
| case Op::kAtomicAdd: |
| ss << "intrinsic_atomic_add_"; |
| break; |
| case Op::kAtomicSub: |
| ss << "intrinsic_atomic_sub_"; |
| break; |
| case Op::kAtomicMax: |
| ss << "intrinsic_atomic_max_"; |
| break; |
| case Op::kAtomicMin: |
| ss << "intrinsic_atomic_min_"; |
| break; |
| case Op::kAtomicAnd: |
| ss << "intrinsic_atomic_and_"; |
| break; |
| case Op::kAtomicOr: |
| ss << "intrinsic_atomic_or_"; |
| break; |
| case Op::kAtomicXor: |
| ss << "intrinsic_atomic_xor_"; |
| break; |
| case Op::kAtomicExchange: |
| ss << "intrinsic_atomic_exchange_"; |
| break; |
| case Op::kAtomicCompareExchangeWeak: |
| ss << "intrinsic_atomic_compare_exchange_weak_"; |
| break; |
| } |
| ss << address_space << "_"; |
| switch (type) { |
| case DataType::kU32: |
| ss << "u32"; |
| break; |
| case DataType::kF32: |
| ss << "f32"; |
| break; |
| case DataType::kI32: |
| ss << "i32"; |
| break; |
| case DataType::kF16: |
| ss << "f16"; |
| break; |
| case DataType::kVec2U32: |
| ss << "vec2_u32"; |
| break; |
| case DataType::kVec2F32: |
| ss << "vec2_f32"; |
| break; |
| case DataType::kVec2I32: |
| ss << "vec2_i32"; |
| break; |
| case DataType::kVec2F16: |
| ss << "vec2_f16"; |
| break; |
| case DataType::kVec3U32: |
| ss << "vec3_u32"; |
| break; |
| case DataType::kVec3F32: |
| ss << "vec3_f32"; |
| break; |
| case DataType::kVec3I32: |
| ss << "vec3_i32"; |
| break; |
| case DataType::kVec3F16: |
| ss << "vec3_f16"; |
| break; |
| case DataType::kVec4U32: |
| ss << "vec4_u32"; |
| break; |
| case DataType::kVec4F32: |
| ss << "vec4_f32"; |
| break; |
| case DataType::kVec4I32: |
| ss << "vec4_i32"; |
| break; |
| case DataType::kVec4F16: |
| ss << "vec4_f16"; |
| break; |
| } |
| return ss.str(); |
| } |
| |
| const DecomposeMemoryAccess::Intrinsic* DecomposeMemoryAccess::Intrinsic::Clone( |
| CloneContext* ctx) const { |
| return ctx->dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( |
| ctx->dst->ID(), ctx->dst->AllocateNodeID(), op, address_space, type); |
| } |
| |
| bool DecomposeMemoryAccess::Intrinsic::IsAtomic() const { |
| return op != Op::kLoad && op != Op::kStore; |
| } |
| |
| DecomposeMemoryAccess::DecomposeMemoryAccess() = default; |
| DecomposeMemoryAccess::~DecomposeMemoryAccess() = default; |
| |
| Transform::ApplyResult DecomposeMemoryAccess::Apply(const Program* src, |
| const DataMap&, |
| DataMap&) const { |
| if (!ShouldRun(src)) { |
| return SkipTransform; |
| } |
| |
| auto& sem = src->Sem(); |
| ProgramBuilder b; |
| CloneContext ctx{&b, src, /* auto_clone_symbols */ true}; |
| State state(ctx); |
| |
| // Scan the AST nodes for storage and uniform buffer accesses. Complex |
| // expression chains (e.g. `storage_buffer.foo.bar[20].x`) are handled by |
| // maintaining an offset chain via the `state.TakeAccess()`, |
| // `state.AddAccess()` methods. |
| // |
| // Inner-most expression nodes are guaranteed to be visited first because AST |
| // nodes are fully immutable and require their children to be constructed |
| // first so their pointer can be passed to the parent's initializer. |
| for (auto* node : src->ASTNodes().Objects()) { |
| if (auto* ident = node->As<ast::IdentifierExpression>()) { |
| // X |
| if (auto* var = sem.Get<sem::VariableUser>(ident)) { |
| if (var->Variable()->AddressSpace() == ast::AddressSpace::kStorage || |
| var->Variable()->AddressSpace() == ast::AddressSpace::kUniform) { |
| // Variable to a storage or uniform buffer |
| state.AddAccess(ident, { |
| var, |
| state.ToOffset(0u), |
| var->Type()->UnwrapRef(), |
| }); |
| } |
| } |
| continue; |
| } |
| |
| if (auto* accessor = node->As<ast::MemberAccessorExpression>()) { |
| // X.Y |
| auto* accessor_sem = sem.Get(accessor); |
| if (auto* swizzle = accessor_sem->As<sem::Swizzle>()) { |
| if (swizzle->Indices().Length() == 1) { |
| if (auto access = state.TakeAccess(accessor->structure)) { |
| auto* vec_ty = access.type->As<type::Vector>(); |
| auto* offset = state.Mul(vec_ty->type()->Size(), swizzle->Indices()[0u]); |
| state.AddAccess(accessor, { |
| access.var, |
| state.Add(access.offset, offset), |
| vec_ty->type()->UnwrapRef(), |
| }); |
| } |
| } |
| } else { |
| if (auto access = state.TakeAccess(accessor->structure)) { |
| auto* str_ty = access.type->As<sem::Struct>(); |
| auto* member = str_ty->FindMember(accessor->member->symbol); |
| auto offset = member->Offset(); |
| state.AddAccess(accessor, { |
| access.var, |
| state.Add(access.offset, offset), |
| member->Type()->UnwrapRef(), |
| }); |
| } |
| } |
| continue; |
| } |
| |
| if (auto* accessor = node->As<ast::IndexAccessorExpression>()) { |
| if (auto access = state.TakeAccess(accessor->object)) { |
| // X[Y] |
| if (auto* arr = access.type->As<type::Array>()) { |
| auto* offset = state.Mul(arr->Stride(), accessor->index); |
| state.AddAccess(accessor, { |
| access.var, |
| state.Add(access.offset, offset), |
| arr->ElemType()->UnwrapRef(), |
| }); |
| continue; |
| } |
| if (auto* vec_ty = access.type->As<type::Vector>()) { |
| auto* offset = state.Mul(vec_ty->type()->Size(), accessor->index); |
| state.AddAccess(accessor, { |
| access.var, |
| state.Add(access.offset, offset), |
| vec_ty->type()->UnwrapRef(), |
| }); |
| continue; |
| } |
| if (auto* mat_ty = access.type->As<type::Matrix>()) { |
| auto* offset = state.Mul(mat_ty->ColumnStride(), accessor->index); |
| state.AddAccess(accessor, { |
| access.var, |
| state.Add(access.offset, offset), |
| mat_ty->ColumnType(), |
| }); |
| continue; |
| } |
| } |
| } |
| |
| if (auto* op = node->As<ast::UnaryOpExpression>()) { |
| if (op->op == ast::UnaryOp::kAddressOf) { |
| // &X |
| if (auto access = state.TakeAccess(op->expr)) { |
| // HLSL does not support pointers, so just take the access from the |
| // reference and place it on the pointer. |
| state.AddAccess(op, access); |
| continue; |
| } |
| } |
| } |
| |
| if (auto* assign = node->As<ast::AssignmentStatement>()) { |
| // X = Y |
| // Move the LHS access to a store. |
| if (auto lhs = state.TakeAccess(assign->lhs)) { |
| state.stores.emplace_back(Store{assign, lhs}); |
| } |
| } |
| |
| if (auto* call_expr = node->As<ast::CallExpression>()) { |
| auto* call = sem.Get(call_expr)->UnwrapMaterialize()->As<sem::Call>(); |
| if (auto* builtin = call->Target()->As<sem::Builtin>()) { |
| if (builtin->Type() == sem::BuiltinType::kArrayLength) { |
| // arrayLength(X) |
| // Don't convert X into a load, this builtin actually requires the real pointer. |
| state.TakeAccess(call_expr->args[0]); |
| continue; |
| } |
| if (builtin->IsAtomic()) { |
| if (auto access = state.TakeAccess(call_expr->args[0])) { |
| // atomic___(X) |
| ctx.Replace(call_expr, [=, &ctx, &state] { |
| auto* buf = access.var->Declaration(); |
| auto* offset = access.offset->Build(ctx); |
| auto* buf_ty = access.var->Type()->UnwrapRef(); |
| auto* el_ty = access.type->UnwrapRef()->As<type::Atomic>()->Type(); |
| Symbol func = state.AtomicFunc(buf_ty, el_ty, builtin, |
| access.var->As<sem::VariableUser>()); |
| |
| utils::Vector<const ast::Expression*, 8> args{ |
| ctx.dst->AddressOf(ctx.Clone(buf)), offset}; |
| for (size_t i = 1; i < call_expr->args.Length(); i++) { |
| auto* arg = call_expr->args[i]; |
| args.Push(ctx.Clone(arg)); |
| } |
| return ctx.dst->Call(func, args); |
| }); |
| } |
| } |
| } |
| } |
| } |
| |
| // All remaining accesses are loads, transform these into calls to the |
| // corresponding load function |
| for (auto* expr : state.expression_order) { |
| auto access_it = state.accesses.find(expr); |
| if (access_it == state.accesses.end()) { |
| continue; |
| } |
| BufferAccess access = access_it->second; |
| ctx.Replace(expr, [=, &ctx, &state] { |
| auto* buf = ctx.dst->AddressOf(ctx.CloneWithoutTransform(access.var->Declaration())); |
| auto* offset = access.offset->Build(ctx); |
| auto* buf_ty = access.var->Type()->UnwrapRef(); |
| auto* el_ty = access.type->UnwrapRef(); |
| Symbol func = state.LoadFunc(buf_ty, el_ty, access.var->As<sem::VariableUser>()); |
| return ctx.dst->Call(func, buf, offset); |
| }); |
| } |
| |
| // And replace all storage and uniform buffer assignments with stores |
| for (auto store : state.stores) { |
| ctx.Replace(store.assignment, [=, &ctx, &state] { |
| auto* buf = |
| ctx.dst->AddressOf(ctx.CloneWithoutTransform((store.target.var->Declaration()))); |
| auto* offset = store.target.offset->Build(ctx); |
| auto* buf_ty = store.target.var->Type()->UnwrapRef(); |
| auto* el_ty = store.target.type->UnwrapRef(); |
| auto* value = store.assignment->rhs; |
| Symbol func = state.StoreFunc(buf_ty, el_ty, store.target.var->As<sem::VariableUser>()); |
| auto* call = ctx.dst->Call(func, buf, offset, ctx.Clone(value)); |
| return ctx.dst->CallStmt(call); |
| }); |
| } |
| |
| ctx.Clone(); |
| return Program(std::move(b)); |
| } |
| |
| } // namespace tint::transform |
| |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::Offset); |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::OffsetLiteral); |