| // 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/transform/decompose_memory_access.h" | |
| #include <memory> | |
| #include <string> | |
| #include <unordered_map> | |
| #include <utility> | |
| #include <vector> | |
| #include "src/ast/assignment_statement.h" | |
| #include "src/ast/call_statement.h" | |
| #include "src/ast/disable_validation_decoration.h" | |
| #include "src/ast/type_name.h" | |
| #include "src/ast/unary_op.h" | |
| #include "src/block_allocator.h" | |
| #include "src/program_builder.h" | |
| #include "src/sem/array.h" | |
| #include "src/sem/atomic_type.h" | |
| #include "src/sem/call.h" | |
| #include "src/sem/member_accessor_expression.h" | |
| #include "src/sem/reference_type.h" | |
| #include "src/sem/statement.h" | |
| #include "src/sem/struct.h" | |
| #include "src/sem/variable.h" | |
| #include "src/utils/map.h" | |
| #include "src/utils/hash.h" | |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::DecomposeMemoryAccess); | |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::DecomposeMemoryAccess::Intrinsic); | |
| namespace tint { | |
| namespace transform { | |
| namespace { | |
| /// 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<sem::U32>()) { | |
| res = ctx.dst->Construct<ProgramBuilder::u32>(res); | |
| } | |
| return res; | |
| } | |
| }; | |
| /// OffsetLiteral is an implementation of Offset that constructs a u32 literal | |
| /// value. | |
| struct OffsetLiteral : 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(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::StorageClass const storage_class; // buffer storage class | |
| sem::Type const* buf_ty = nullptr; // buffer type | |
| sem::Type const* el_ty = nullptr; // element type | |
| bool operator==(const LoadStoreKey& rhs) const { | |
| return storage_class == rhs.storage_class && 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.storage_class, u.buf_ty, u.el_ty); | |
| } | |
| }; | |
| }; | |
| /// AtomicKey is the unordered map key to an atomic intrinsic. | |
| struct AtomicKey { | |
| sem::Type const* buf_ty = nullptr; // buffer type | |
| sem::Type const* el_ty = nullptr; // element type | |
| sem::IntrinsicType const op; // atomic op | |
| bool operator==(const AtomicKey& rhs) const { | |
| return 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.buf_ty, u.el_ty, u.op); | |
| } | |
| }; | |
| }; | |
| bool IntrinsicDataTypeFor(const sem::Type* ty, | |
| DecomposeMemoryAccess::Intrinsic::DataType& out) { | |
| if (ty->Is<sem::I32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kI32; | |
| return true; | |
| } | |
| if (ty->Is<sem::U32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kU32; | |
| return true; | |
| } | |
| if (ty->Is<sem::F32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kF32; | |
| return true; | |
| } | |
| if (auto* vec = ty->As<sem::Vector>()) { | |
| switch (vec->Width()) { | |
| case 2: | |
| if (vec->type()->Is<sem::I32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2I32; | |
| return true; | |
| } | |
| if (vec->type()->Is<sem::U32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2U32; | |
| return true; | |
| } | |
| if (vec->type()->Is<sem::F32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec2F32; | |
| return true; | |
| } | |
| break; | |
| case 3: | |
| if (vec->type()->Is<sem::I32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3I32; | |
| return true; | |
| } | |
| if (vec->type()->Is<sem::U32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3U32; | |
| return true; | |
| } | |
| if (vec->type()->Is<sem::F32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec3F32; | |
| return true; | |
| } | |
| break; | |
| case 4: | |
| if (vec->type()->Is<sem::I32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4I32; | |
| return true; | |
| } | |
| if (vec->type()->Is<sem::U32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4U32; | |
| return true; | |
| } | |
| if (vec->type()->Is<sem::F32>()) { | |
| out = DecomposeMemoryAccess::Intrinsic::DataType::kVec4F32; | |
| return true; | |
| } | |
| break; | |
| } | |
| return false; | |
| } | |
| return false; | |
| } | |
| /// @returns a DecomposeMemoryAccess::Intrinsic decoration that can be applied | |
| /// to a stub function to load the type `ty`. | |
| DecomposeMemoryAccess::Intrinsic* IntrinsicLoadFor( | |
| ProgramBuilder* builder, | |
| ast::StorageClass storage_class, | |
| const sem::Type* ty) { | |
| DecomposeMemoryAccess::Intrinsic::DataType type; | |
| if (!IntrinsicDataTypeFor(ty, type)) { | |
| return nullptr; | |
| } | |
| return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( | |
| builder->ID(), DecomposeMemoryAccess::Intrinsic::Op::kLoad, storage_class, | |
| type); | |
| } | |
| /// @returns a DecomposeMemoryAccess::Intrinsic decoration that can be applied | |
| /// to a stub function to store the type `ty`. | |
| DecomposeMemoryAccess::Intrinsic* IntrinsicStoreFor( | |
| ProgramBuilder* builder, | |
| ast::StorageClass storage_class, | |
| const sem::Type* ty) { | |
| DecomposeMemoryAccess::Intrinsic::DataType type; | |
| if (!IntrinsicDataTypeFor(ty, type)) { | |
| return nullptr; | |
| } | |
| return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( | |
| builder->ID(), DecomposeMemoryAccess::Intrinsic::Op::kStore, | |
| storage_class, type); | |
| } | |
| /// @returns a DecomposeMemoryAccess::Intrinsic decoration that can be applied | |
| /// to a stub function for the atomic op and the type `ty`. | |
| DecomposeMemoryAccess::Intrinsic* IntrinsicAtomicFor(ProgramBuilder* builder, | |
| sem::IntrinsicType ity, | |
| const sem::Type* ty) { | |
| auto op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicLoad; | |
| switch (ity) { | |
| case sem::IntrinsicType::kAtomicLoad: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicLoad; | |
| break; | |
| case sem::IntrinsicType::kAtomicStore: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicStore; | |
| break; | |
| case sem::IntrinsicType::kAtomicAdd: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicAdd; | |
| break; | |
| case sem::IntrinsicType::kAtomicSub: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicSub; | |
| break; | |
| case sem::IntrinsicType::kAtomicMax: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicMax; | |
| break; | |
| case sem::IntrinsicType::kAtomicMin: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicMin; | |
| break; | |
| case sem::IntrinsicType::kAtomicAnd: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicAnd; | |
| break; | |
| case sem::IntrinsicType::kAtomicOr: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicOr; | |
| break; | |
| case sem::IntrinsicType::kAtomicXor: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicXor; | |
| break; | |
| case sem::IntrinsicType::kAtomicExchange: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicExchange; | |
| break; | |
| case sem::IntrinsicType::kAtomicCompareExchangeWeak: | |
| op = DecomposeMemoryAccess::Intrinsic::Op::kAtomicCompareExchangeWeak; | |
| break; | |
| default: | |
| TINT_ICE(Transform, builder->Diagnostics()) | |
| << "invalid IntrinsicType for DecomposeMemoryAccess::Intrinsic: " | |
| << ty->type_name(); | |
| break; | |
| } | |
| DecomposeMemoryAccess::Intrinsic::DataType type; | |
| if (!IntrinsicDataTypeFor(ty, type)) { | |
| return nullptr; | |
| } | |
| return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( | |
| builder->ID(), op, ast::StorageClass::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 | |
| sem::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 { | |
| ast::AssignmentStatement* assignment; // The AST assignment statement | |
| BufferAccess target; // The target for the write | |
| }; | |
| } // namespace | |
| /// State holds the current transform state | |
| 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 | |
| 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* u32 = expr->As<ast::UintLiteralExpression>()) { | |
| return offsets_.Create<OffsetLiteral>(u32->value); | |
| } else if (auto* i32 = expr->As<ast::SintLiteralExpression>()) { | |
| if (i32->value > 0) { | |
| return offsets_.Create<OffsetLiteral>(i32->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>(0); | |
| } | |
| if (rhs_lit && rhs_lit->literal == 0) { | |
| return offsets_.Create<OffsetLiteral>(0); | |
| } | |
| 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 : buf_ty, 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 sem::Type* buf_ty, | |
| const sem::Type* el_ty, | |
| const sem::VariableUser* var_user) { | |
| auto storage_class = var_user->Variable()->StorageClass(); | |
| return utils::GetOrCreate( | |
| load_funcs, LoadStoreKey{storage_class, buf_ty, el_ty}, [&] { | |
| auto* buf_ast_ty = CreateASTTypeFor(ctx, buf_ty); | |
| auto* disable_validation = b.Disable( | |
| ast::DisabledValidation::kIgnoreConstructibleFunctionParameter); | |
| ast::VariableList params = { | |
| // Note: The buffer parameter requires the StorageClass in | |
| // order for HLSL to emit this as a ByteAddressBuffer or cbuffer | |
| // array. | |
| b.create<ast::Variable>(b.Sym("buffer"), storage_class, | |
| var_user->Variable()->Access(), | |
| buf_ast_ty, true, nullptr, | |
| ast::DecorationList{disable_validation}), | |
| b.Param("offset", b.ty.u32()), | |
| }; | |
| auto name = b.Sym(); | |
| if (auto* intrinsic = | |
| IntrinsicLoadFor(ctx.dst, storage_class, el_ty)) { | |
| auto* el_ast_ty = CreateASTTypeFor(ctx, el_ty); | |
| auto* func = b.create<ast::Function>( | |
| name, params, el_ast_ty, nullptr, | |
| ast::DecorationList{ | |
| intrinsic, | |
| b.Disable(ast::DisabledValidation::kFunctionHasNoBody), | |
| }, | |
| ast::DecorationList{}); | |
| b.AST().AddFunction(func); | |
| } else if (auto* arr_ty = el_ty->As<sem::Array>()) { | |
| // fn load_func(buf : 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(buf, 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"), nullptr, b.Expr(0u)); | |
| auto* for_init = b.Decl(i); | |
| auto* for_cond = b.create<ast::BinaryExpression>( | |
| ast::BinaryOp::kLessThan, b.Expr(i), b.Expr(arr_ty->Count())); | |
| auto* for_cont = b.Assign(i, b.Add(i, 1u)); | |
| auto* arr_el = b.IndexAccessor(arr, i); | |
| auto* el_offset = | |
| b.Add(b.Expr("offset"), b.Mul(i, 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), | |
| { | |
| b.Decl(arr), | |
| for_loop, | |
| b.Return(arr), | |
| }); | |
| } else { | |
| ast::ExpressionList values; | |
| if (auto* mat_ty = el_ty->As<sem::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", i * mat_ty->ColumnStride()); | |
| values.emplace_back(b.Call(load, "buffer", offset)); | |
| } | |
| } else if (auto* str = el_ty->As<sem::Struct>()) { | |
| for (auto* member : str->Members()) { | |
| auto* offset = b.Add("offset", member->Offset()); | |
| Symbol load = | |
| LoadFunc(buf_ty, member->Type()->UnwrapRef(), var_user); | |
| values.emplace_back(b.Call(load, "buffer", offset)); | |
| } | |
| } | |
| b.Func( | |
| name, params, CreateASTTypeFor(ctx, el_ty), | |
| { | |
| 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 : buf_ty, 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 sem::Type* buf_ty, | |
| const sem::Type* el_ty, | |
| const sem::VariableUser* var_user) { | |
| auto storage_class = var_user->Variable()->StorageClass(); | |
| return utils::GetOrCreate( | |
| store_funcs, LoadStoreKey{storage_class, buf_ty, el_ty}, [&] { | |
| auto* buf_ast_ty = CreateASTTypeFor(ctx, buf_ty); | |
| auto* el_ast_ty = CreateASTTypeFor(ctx, el_ty); | |
| auto* disable_validation = b.Disable( | |
| ast::DisabledValidation::kIgnoreConstructibleFunctionParameter); | |
| ast::VariableList params{ | |
| // Note: The buffer parameter requires the StorageClass in | |
| // order for HLSL to emit this as a ByteAddressBuffer. | |
| b.create<ast::Variable>(b.Sym("buffer"), storage_class, | |
| var_user->Variable()->Access(), | |
| buf_ast_ty, true, nullptr, | |
| ast::DecorationList{disable_validation}), | |
| b.Param("offset", b.ty.u32()), | |
| b.Param("value", el_ast_ty), | |
| }; | |
| auto name = b.Sym(); | |
| if (auto* intrinsic = | |
| IntrinsicStoreFor(ctx.dst, storage_class, el_ty)) { | |
| auto* func = b.create<ast::Function>( | |
| name, params, b.ty.void_(), nullptr, | |
| ast::DecorationList{ | |
| intrinsic, | |
| b.Disable(ast::DisabledValidation::kFunctionHasNoBody), | |
| }, | |
| ast::DecorationList{}); | |
| b.AST().AddFunction(func); | |
| } else { | |
| ast::StatementList body; | |
| if (auto* arr_ty = el_ty->As<sem::Array>()) { | |
| // fn store_func(buf : 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(buf, offset + i * array_stride, | |
| // value[i]) | |
| // } | |
| // return arr; | |
| // } | |
| auto* array = | |
| b.Var(b.Symbols().New("array"), nullptr, b.Expr("value")); | |
| auto store = | |
| StoreFunc(buf_ty, arr_ty->ElemType()->UnwrapRef(), var_user); | |
| auto* i = b.Var(b.Symbols().New("i"), nullptr, b.Expr(0u)); | |
| auto* for_init = b.Decl(i); | |
| auto* for_cond = b.create<ast::BinaryExpression>( | |
| ast::BinaryOp::kLessThan, b.Expr(i), b.Expr(arr_ty->Count())); | |
| auto* for_cont = b.Assign(i, b.Add(i, 1u)); | |
| auto* arr_el = b.IndexAccessor(array, i); | |
| auto* el_offset = | |
| b.Add(b.Expr("offset"), b.Mul(i, 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)); | |
| body = {b.Decl(array), for_loop}; | |
| } else if (auto* mat_ty = el_ty->As<sem::Matrix>()) { | |
| auto* vec_ty = mat_ty->ColumnType(); | |
| Symbol store = StoreFunc(buf_ty, vec_ty, var_user); | |
| for (uint32_t i = 0; i < mat_ty->columns(); i++) { | |
| auto* offset = b.Add("offset", i * mat_ty->ColumnStride()); | |
| auto* access = b.IndexAccessor("value", i); | |
| auto* call = b.Call(store, "buffer", offset, access); | |
| body.emplace_back(b.CallStmt(call)); | |
| } | |
| } else if (auto* str = el_ty->As<sem::Struct>()) { | |
| for (auto* member : str->Members()) { | |
| auto* offset = b.Add("offset", member->Offset()); | |
| auto* access = b.MemberAccessor( | |
| "value", ctx.Clone(member->Declaration()->symbol)); | |
| Symbol store = | |
| StoreFunc(buf_ty, member->Type()->UnwrapRef(), var_user); | |
| auto* call = b.Call(store, "buffer", offset, access); | |
| body.emplace_back(b.CallStmt(call)); | |
| } | |
| } | |
| 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 : buf_ty, 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 sem::Type* buf_ty, | |
| const sem::Type* el_ty, | |
| const sem::Intrinsic* intrinsic, | |
| const sem::VariableUser* var_user) { | |
| auto op = intrinsic->Type(); | |
| return utils::GetOrCreate(atomic_funcs, AtomicKey{buf_ty, el_ty, op}, [&] { | |
| auto* buf_ast_ty = CreateASTTypeFor(ctx, buf_ty); | |
| auto* disable_validation = b.Disable( | |
| ast::DisabledValidation::kIgnoreConstructibleFunctionParameter); | |
| // The first parameter to all WGSL atomics is the expression to the | |
| // atomic. This is replaced with two parameters: the buffer and offset. | |
| ast::VariableList params = { | |
| // Note: The buffer parameter requires the kStorage StorageClass in | |
| // order for HLSL to emit this as a ByteAddressBuffer. | |
| b.create<ast::Variable>(b.Sym("buffer"), ast::StorageClass::kStorage, | |
| var_user->Variable()->Access(), buf_ast_ty, | |
| true, nullptr, | |
| ast::DecorationList{disable_validation}), | |
| b.Param("offset", b.ty.u32()), | |
| }; | |
| // Other parameters are copied as-is: | |
| for (size_t i = 1; i < intrinsic->Parameters().size(); i++) { | |
| auto* param = intrinsic->Parameters()[i]; | |
| auto* ty = CreateASTTypeFor(ctx, param->Type()); | |
| params.emplace_back(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->type_name(); | |
| } | |
| auto* ret_ty = CreateASTTypeFor(ctx, intrinsic->ReturnType()); | |
| auto* func = b.create<ast::Function>( | |
| b.Sym(), params, ret_ty, nullptr, | |
| ast::DecorationList{ | |
| atomic, | |
| b.Disable(ast::DisabledValidation::kFunctionHasNoBody), | |
| }, | |
| ast::DecorationList{}); | |
| b.AST().AddFunction(func); | |
| return func->symbol; | |
| }); | |
| } | |
| }; | |
| DecomposeMemoryAccess::Intrinsic::Intrinsic(ProgramID pid, | |
| Op o, | |
| ast::StorageClass sc, | |
| DataType ty) | |
| : Base(pid), op(o), storage_class(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 << storage_class << "_"; | |
| switch (type) { | |
| case DataType::kU32: | |
| ss << "u32"; | |
| break; | |
| case DataType::kF32: | |
| ss << "f32"; | |
| break; | |
| case DataType::kI32: | |
| ss << "i32"; | |
| break; | |
| case DataType::kVec2U32: | |
| ss << "vec2_u32"; | |
| break; | |
| case DataType::kVec2F32: | |
| ss << "vec2_f32"; | |
| break; | |
| case DataType::kVec2I32: | |
| ss << "vec2_i32"; | |
| break; | |
| case DataType::kVec3U32: | |
| ss << "vec3_u32"; | |
| break; | |
| case DataType::kVec3F32: | |
| ss << "vec3_f32"; | |
| break; | |
| case DataType::kVec3I32: | |
| ss << "vec3_i32"; | |
| break; | |
| case DataType::kVec4U32: | |
| ss << "vec4_u32"; | |
| break; | |
| case DataType::kVec4F32: | |
| ss << "vec4_f32"; | |
| break; | |
| case DataType::kVec4I32: | |
| ss << "vec4_i32"; | |
| break; | |
| } | |
| return ss.str(); | |
| } | |
| const DecomposeMemoryAccess::Intrinsic* DecomposeMemoryAccess::Intrinsic::Clone( | |
| CloneContext* ctx) const { | |
| return ctx->dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>( | |
| ctx->dst->ID(), op, storage_class, type); | |
| } | |
| DecomposeMemoryAccess::DecomposeMemoryAccess() = default; | |
| DecomposeMemoryAccess::~DecomposeMemoryAccess() = default; | |
| void DecomposeMemoryAccess::Run(CloneContext& ctx, const DataMap&, DataMap&) { | |
| auto& sem = ctx.src->Sem(); | |
| 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 constructor. | |
| for (auto* node : ctx.src->ASTNodes().Objects()) { | |
| if (auto* ident = node->As<ast::IdentifierExpression>()) { | |
| // X | |
| if (auto* var = sem.Get<sem::VariableUser>(ident)) { | |
| if (var->Variable()->StorageClass() == ast::StorageClass::kStorage || | |
| var->Variable()->StorageClass() == ast::StorageClass::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().size() == 1) { | |
| if (auto access = state.TakeAccess(accessor->structure)) { | |
| auto* vec_ty = access.type->As<sem::Vector>(); | |
| auto* offset = | |
| state.Mul(vec_ty->type()->Size(), swizzle->Indices()[0]); | |
| 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<sem::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<sem::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<sem::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); | |
| if (auto* intrinsic = call->Target()->As<sem::Intrinsic>()) { | |
| if (intrinsic->Type() == sem::IntrinsicType::kIgnore) { // [DEPRECATED] | |
| // ignore(X) | |
| // If X is an memory access, don't transform it into a load, as it | |
| // may refer to a structure holding a runtime array, which cannot be | |
| // loaded. Instead replace X with the underlying storage / uniform | |
| // buffer variable. | |
| if (auto access = state.TakeAccess(call_expr->args[0])) { | |
| ctx.Replace(call_expr->args[0], [=, &ctx] { | |
| return ctx.CloneWithoutTransform(access.var->Declaration()); | |
| }); | |
| } | |
| continue; | |
| } | |
| if (intrinsic->Type() == sem::IntrinsicType::kArrayLength) { | |
| // arrayLength(X) | |
| // Don't convert X into a load, this intrinsic actually requires the | |
| // real pointer. | |
| state.TakeAccess(call_expr->args[0]); | |
| continue; | |
| } | |
| if (intrinsic->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<sem::Atomic>()->Type(); | |
| Symbol func = | |
| state.AtomicFunc(buf_ty, el_ty, intrinsic, | |
| access.var->As<sem::VariableUser>()); | |
| ast::ExpressionList args{ctx.Clone(buf), offset}; | |
| for (size_t i = 1; i < call_expr->args.size(); i++) { | |
| auto* arg = call_expr->args[i]; | |
| args.emplace_back(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 = 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, ctx.CloneWithoutTransform(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 = 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, ctx.CloneWithoutTransform(buf), offset, | |
| ctx.Clone(value)); | |
| return ctx.dst->CallStmt(call); | |
| }); | |
| } | |
| ctx.Clone(); | |
| } | |
| } // namespace transform | |
| } // namespace tint | |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::Offset); | |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::OffsetLiteral); |