| // 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/canonicalize_entry_point_io.h" | |
| #include <algorithm> | |
| #include <string> | |
| #include <unordered_set> | |
| #include <utility> | |
| #include <vector> | |
| #include "src/tint/ast/disable_validation_attribute.h" | |
| #include "src/tint/builtin/builtin_value.h" | |
| #include "src/tint/program_builder.h" | |
| #include "src/tint/sem/function.h" | |
| #include "src/tint/transform/unshadow.h" | |
| using namespace tint::number_suffixes; // NOLINT | |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::CanonicalizeEntryPointIO); | |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::CanonicalizeEntryPointIO::Config); | |
| namespace tint::transform { | |
| CanonicalizeEntryPointIO::CanonicalizeEntryPointIO() = default; | |
| CanonicalizeEntryPointIO::~CanonicalizeEntryPointIO() = default; | |
| namespace { | |
| /// Info for a struct member | |
| struct MemberInfo { | |
| /// The struct member item | |
| const ast::StructMember* member; | |
| /// The struct member location if provided | |
| std::optional<uint32_t> location; | |
| }; | |
| /// FXC is sensitive to field order in structures, this is used by StructMemberComparator to ensure | |
| /// that FXC is happy with the order of emitted fields. | |
| uint32_t BuiltinOrder(builtin::BuiltinValue builtin) { | |
| switch (builtin) { | |
| case builtin::BuiltinValue::kPosition: | |
| return 1; | |
| case builtin::BuiltinValue::kVertexIndex: | |
| return 2; | |
| case builtin::BuiltinValue::kInstanceIndex: | |
| return 3; | |
| case builtin::BuiltinValue::kFrontFacing: | |
| return 4; | |
| case builtin::BuiltinValue::kFragDepth: | |
| return 5; | |
| case builtin::BuiltinValue::kLocalInvocationId: | |
| return 6; | |
| case builtin::BuiltinValue::kLocalInvocationIndex: | |
| return 7; | |
| case builtin::BuiltinValue::kGlobalInvocationId: | |
| return 8; | |
| case builtin::BuiltinValue::kWorkgroupId: | |
| return 9; | |
| case builtin::BuiltinValue::kNumWorkgroups: | |
| return 10; | |
| case builtin::BuiltinValue::kSampleIndex: | |
| return 11; | |
| case builtin::BuiltinValue::kSampleMask: | |
| return 12; | |
| case builtin::BuiltinValue::kPointSize: | |
| return 13; | |
| default: | |
| break; | |
| } | |
| return 0; | |
| } | |
| // Returns true if `attr` is a shader IO attribute. | |
| bool IsShaderIOAttribute(const ast::Attribute* attr) { | |
| return attr->IsAnyOf<ast::BuiltinAttribute, ast::InterpolateAttribute, ast::InvariantAttribute, | |
| ast::LocationAttribute>(); | |
| } | |
| } // namespace | |
| /// PIMPL state for the transform | |
| struct CanonicalizeEntryPointIO::State { | |
| /// OutputValue represents a shader result that the wrapper function produces. | |
| struct OutputValue { | |
| /// The name of the output value. | |
| std::string name; | |
| /// The type of the output value. | |
| ast::Type type; | |
| /// The shader IO attributes. | |
| utils::Vector<const ast::Attribute*, 8> attributes; | |
| /// The value itself. | |
| const ast::Expression* value; | |
| /// The output location. | |
| std::optional<uint32_t> location; | |
| }; | |
| /// The clone context. | |
| CloneContext& ctx; | |
| /// The transform config. | |
| CanonicalizeEntryPointIO::Config const cfg; | |
| /// The entry point function (AST). | |
| const ast::Function* func_ast; | |
| /// The entry point function (SEM). | |
| const sem::Function* func_sem; | |
| /// The new entry point wrapper function's parameters. | |
| utils::Vector<const ast::Parameter*, 8> wrapper_ep_parameters; | |
| /// The members of the wrapper function's struct parameter. | |
| utils::Vector<MemberInfo, 8> wrapper_struct_param_members; | |
| /// The name of the wrapper function's struct parameter. | |
| Symbol wrapper_struct_param_name; | |
| /// The parameters that will be passed to the original function. | |
| utils::Vector<const ast::Expression*, 8> inner_call_parameters; | |
| /// The members of the wrapper function's struct return type. | |
| utils::Vector<MemberInfo, 8> wrapper_struct_output_members; | |
| /// The wrapper function output values. | |
| utils::Vector<OutputValue, 8> wrapper_output_values; | |
| /// The body of the wrapper function. | |
| utils::Vector<const ast::Statement*, 8> wrapper_body; | |
| /// Input names used by the entrypoint | |
| std::unordered_set<std::string> input_names; | |
| /// A map of cloned attribute to builtin value | |
| utils::Hashmap<const ast::BuiltinAttribute*, builtin::BuiltinValue, 16> builtin_attrs; | |
| /// Constructor | |
| /// @param context the clone context | |
| /// @param config the transform config | |
| /// @param function the entry point function | |
| State(CloneContext& context, | |
| const CanonicalizeEntryPointIO::Config& config, | |
| const ast::Function* function) | |
| : ctx(context), cfg(config), func_ast(function), func_sem(ctx.src->Sem().Get(function)) {} | |
| /// Clones the attributes from @p in and adds it to @p out. If @p in is a builtin attribute, | |
| /// then builtin_attrs is updated with the builtin information. | |
| /// @param in the attribute to clone | |
| /// @param out the output Attributes | |
| template <size_t N> | |
| void CloneAttribute(const ast::Attribute* in, utils::Vector<const ast::Attribute*, N>& out) { | |
| auto* cloned = ctx.Clone(in); | |
| out.Push(cloned); | |
| if (auto* builtin = in->As<ast::BuiltinAttribute>()) { | |
| builtin_attrs.Add(cloned->As<ast::BuiltinAttribute>(), | |
| ctx.src->Sem().Get(builtin)->Value()); | |
| } | |
| } | |
| /// Clones the shader IO attributes from @p in. | |
| /// @param in the attributes to clone | |
| /// @param do_interpolate whether to clone InterpolateAttribute | |
| /// @return the cloned attributes | |
| template <size_t N> | |
| auto CloneShaderIOAttributes(const utils::Vector<const ast::Attribute*, N> in, | |
| bool do_interpolate) { | |
| utils::Vector<const ast::Attribute*, N> out; | |
| for (auto* attr : in) { | |
| if (IsShaderIOAttribute(attr) && | |
| (do_interpolate || !attr->template Is<ast::InterpolateAttribute>())) { | |
| CloneAttribute(attr, out); | |
| } | |
| } | |
| return out; | |
| } | |
| /// @param attr the input attribute | |
| /// @returns the builtin value of the attribute | |
| builtin::BuiltinValue BuiltinOf(const ast::BuiltinAttribute* attr) { | |
| if (attr->program_id == ctx.dst->ID()) { | |
| // attr belongs to the target program. | |
| // Obtain the builtin value from #builtin_attrs. | |
| if (auto b = builtin_attrs.Get(attr)) { | |
| return *b; | |
| } | |
| } else { | |
| // attr belongs to the source program. | |
| // Obtain the builtin value from the semantic info. | |
| return ctx.src->Sem().Get(attr)->Value(); | |
| } | |
| TINT_ICE(Resolver, ctx.dst->Diagnostics()) | |
| << "could not obtain builtin value from attribute"; | |
| return builtin::BuiltinValue::kUndefined; | |
| } | |
| /// @param attrs the input attribute list | |
| /// @returns the builtin value if any of the attributes in @p attrs is a builtin attribute, | |
| /// otherwise builtin::BuiltinValue::kUndefined | |
| builtin::BuiltinValue BuiltinOf(utils::VectorRef<const ast::Attribute*> attrs) { | |
| if (auto* builtin = ast::GetAttribute<ast::BuiltinAttribute>(attrs)) { | |
| return BuiltinOf(builtin); | |
| } | |
| return builtin::BuiltinValue::kUndefined; | |
| } | |
| /// Create or return a symbol for the wrapper function's struct parameter. | |
| /// @returns the symbol for the struct parameter | |
| Symbol InputStructSymbol() { | |
| if (!wrapper_struct_param_name.IsValid()) { | |
| wrapper_struct_param_name = ctx.dst->Sym(); | |
| } | |
| return wrapper_struct_param_name; | |
| } | |
| /// Add a shader input to the entry point. | |
| /// @param name the name of the shader input | |
| /// @param type the type of the shader input | |
| /// @param location the location if provided | |
| /// @param attrs the attributes to apply to the shader input | |
| /// @returns an expression which evaluates to the value of the shader input | |
| const ast::Expression* AddInput(std::string name, | |
| const type::Type* type, | |
| std::optional<uint32_t> location, | |
| utils::Vector<const ast::Attribute*, 8> attrs) { | |
| auto ast_type = CreateASTTypeFor(ctx, type); | |
| auto builtin_attr = BuiltinOf(attrs); | |
| if (cfg.shader_style == ShaderStyle::kSpirv || cfg.shader_style == ShaderStyle::kGlsl) { | |
| // Vulkan requires that integer user-defined fragment inputs are always decorated with | |
| // `Flat`. See: | |
| // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/StandaloneSpirv.html#VUID-StandaloneSpirv-Flat-04744 | |
| // TODO(crbug.com/tint/1224): Remove this once a flat interpolation attribute is | |
| // required for integers. | |
| if (func_ast->PipelineStage() == ast::PipelineStage::kFragment && | |
| type->is_integer_scalar_or_vector() && | |
| !ast::HasAttribute<ast::InterpolateAttribute>(attrs) && | |
| (ast::HasAttribute<ast::LocationAttribute>(attrs) || | |
| cfg.shader_style == ShaderStyle::kSpirv)) { | |
| attrs.Push(ctx.dst->Interpolate(builtin::InterpolationType::kFlat, | |
| builtin::InterpolationSampling::kUndefined)); | |
| } | |
| // Disable validation for use of the `input` address space. | |
| attrs.Push(ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace)); | |
| // In GLSL, if it's a builtin, override the name with the | |
| // corresponding gl_ builtin name | |
| if (cfg.shader_style == ShaderStyle::kGlsl && | |
| builtin_attr != builtin::BuiltinValue::kUndefined) { | |
| name = GLSLBuiltinToString(builtin_attr, func_ast->PipelineStage(), | |
| builtin::AddressSpace::kIn); | |
| } | |
| auto symbol = ctx.dst->Symbols().New(name); | |
| // Create the global variable and use its value for the shader input. | |
| const ast::Expression* value = ctx.dst->Expr(symbol); | |
| if (builtin_attr != builtin::BuiltinValue::kUndefined) { | |
| if (cfg.shader_style == ShaderStyle::kGlsl) { | |
| value = FromGLSLBuiltin(builtin_attr, value, ast_type); | |
| } else if (builtin_attr == builtin::BuiltinValue::kSampleMask) { | |
| // Vulkan requires the type of a SampleMask builtin to be an array. | |
| // Declare it as array<u32, 1> and then load the first element. | |
| ast_type = ctx.dst->ty.array(ast_type, 1_u); | |
| value = ctx.dst->IndexAccessor(value, 0_i); | |
| } | |
| } | |
| ctx.dst->GlobalVar(symbol, ast_type, builtin::AddressSpace::kIn, std::move(attrs)); | |
| return value; | |
| } else if (cfg.shader_style == ShaderStyle::kMsl && | |
| builtin_attr != builtin::BuiltinValue::kUndefined) { | |
| // If this input is a builtin and we are targeting MSL, then add it to the | |
| // parameter list and pass it directly to the inner function. | |
| Symbol symbol = input_names.emplace(name).second ? ctx.dst->Symbols().Register(name) | |
| : ctx.dst->Symbols().New(name); | |
| wrapper_ep_parameters.Push(ctx.dst->Param(symbol, ast_type, std::move(attrs))); | |
| return ctx.dst->Expr(symbol); | |
| } else { | |
| // Otherwise, move it to the new structure member list. | |
| Symbol symbol = input_names.emplace(name).second ? ctx.dst->Symbols().Register(name) | |
| : ctx.dst->Symbols().New(name); | |
| wrapper_struct_param_members.Push( | |
| {ctx.dst->Member(symbol, ast_type, std::move(attrs)), location}); | |
| return ctx.dst->MemberAccessor(InputStructSymbol(), symbol); | |
| } | |
| } | |
| /// Add a shader output to the entry point. | |
| /// @param name the name of the shader output | |
| /// @param type the type of the shader output | |
| /// @param location the location if provided | |
| /// @param attrs the attributes to apply to the shader output | |
| /// @param value the value of the shader output | |
| void AddOutput(std::string name, | |
| const type::Type* type, | |
| std::optional<uint32_t> location, | |
| utils::Vector<const ast::Attribute*, 8> attrs, | |
| const ast::Expression* value) { | |
| auto builtin_attr = BuiltinOf(attrs); | |
| // Vulkan requires that integer user-defined vertex outputs are always decorated with | |
| // `Flat`. | |
| // TODO(crbug.com/tint/1224): Remove this once a flat interpolation attribute is required | |
| // for integers. | |
| if (cfg.shader_style == ShaderStyle::kSpirv && | |
| func_ast->PipelineStage() == ast::PipelineStage::kVertex && | |
| type->is_integer_scalar_or_vector() && | |
| ast::HasAttribute<ast::LocationAttribute>(attrs) && | |
| !ast::HasAttribute<ast::InterpolateAttribute>(attrs)) { | |
| attrs.Push(ctx.dst->Interpolate(builtin::InterpolationType::kFlat, | |
| builtin::InterpolationSampling::kUndefined)); | |
| } | |
| // In GLSL, if it's a builtin, override the name with the | |
| // corresponding gl_ builtin name | |
| if (cfg.shader_style == ShaderStyle::kGlsl) { | |
| if (builtin_attr != builtin::BuiltinValue::kUndefined) { | |
| name = GLSLBuiltinToString(builtin_attr, func_ast->PipelineStage(), | |
| builtin::AddressSpace::kOut); | |
| value = ToGLSLBuiltin(builtin_attr, value, type); | |
| } | |
| } | |
| OutputValue output; | |
| output.name = name; | |
| output.type = CreateASTTypeFor(ctx, type); | |
| output.attributes = std::move(attrs); | |
| output.value = value; | |
| output.location = location; | |
| wrapper_output_values.Push(output); | |
| } | |
| /// Process a non-struct parameter. | |
| /// This creates a new object for the shader input, moving the shader IO | |
| /// attributes to it. It also adds an expression to the list of parameters | |
| /// that will be passed to the original function. | |
| /// @param param the original function parameter | |
| void ProcessNonStructParameter(const sem::Parameter* param) { | |
| // Do not add interpolation attributes on vertex input | |
| bool do_interpolate = func_ast->PipelineStage() != ast::PipelineStage::kVertex; | |
| // Remove the shader IO attributes from the inner function parameter, and attach them to the | |
| // new object instead. | |
| utils::Vector<const ast::Attribute*, 8> attributes; | |
| for (auto* attr : param->Declaration()->attributes) { | |
| if (IsShaderIOAttribute(attr)) { | |
| ctx.Remove(param->Declaration()->attributes, attr); | |
| if ((do_interpolate || !attr->Is<ast::InterpolateAttribute>())) { | |
| CloneAttribute(attr, attributes); | |
| } | |
| } | |
| } | |
| auto name = param->Declaration()->name->symbol.Name(); | |
| auto* input_expr = AddInput(name, param->Type(), param->Location(), std::move(attributes)); | |
| inner_call_parameters.Push(input_expr); | |
| } | |
| /// Process a struct parameter. | |
| /// This creates new objects for each struct member, moving the shader IO | |
| /// attributes to them. It also creates the structure that will be passed to | |
| /// the original function. | |
| /// @param param the original function parameter | |
| void ProcessStructParameter(const sem::Parameter* param) { | |
| // Do not add interpolation attributes on vertex input | |
| bool do_interpolate = func_ast->PipelineStage() != ast::PipelineStage::kVertex; | |
| auto* str = param->Type()->As<sem::Struct>(); | |
| // Recreate struct members in the outer entry point and build an initializer | |
| // list to pass them through to the inner function. | |
| utils::Vector<const ast::Expression*, 8> inner_struct_values; | |
| for (auto* member : str->Members()) { | |
| if (TINT_UNLIKELY(member->Type()->Is<type::Struct>())) { | |
| TINT_ICE(Transform, ctx.dst->Diagnostics()) << "nested IO struct"; | |
| continue; | |
| } | |
| auto name = member->Name().Name(); | |
| auto attributes = | |
| CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate); | |
| auto* input_expr = AddInput(name, member->Type(), member->Attributes().location, | |
| std::move(attributes)); | |
| inner_struct_values.Push(input_expr); | |
| } | |
| // Construct the original structure using the new shader input objects. | |
| inner_call_parameters.Push( | |
| ctx.dst->Call(ctx.Clone(param->Declaration()->type), inner_struct_values)); | |
| } | |
| /// Process the entry point return type. | |
| /// This generates a list of output values that are returned by the original | |
| /// function. | |
| /// @param inner_ret_type the original function return type | |
| /// @param original_result the result object produced by the original function | |
| void ProcessReturnType(const type::Type* inner_ret_type, Symbol original_result) { | |
| // Do not add interpolation attributes on fragment output | |
| bool do_interpolate = func_ast->PipelineStage() != ast::PipelineStage::kFragment; | |
| if (auto* str = inner_ret_type->As<sem::Struct>()) { | |
| for (auto* member : str->Members()) { | |
| if (TINT_UNLIKELY(member->Type()->Is<type::Struct>())) { | |
| TINT_ICE(Transform, ctx.dst->Diagnostics()) << "nested IO struct"; | |
| continue; | |
| } | |
| auto name = member->Name().Name(); | |
| auto attributes = | |
| CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate); | |
| // Extract the original structure member. | |
| AddOutput(name, member->Type(), member->Attributes().location, | |
| std::move(attributes), ctx.dst->MemberAccessor(original_result, name)); | |
| } | |
| } else if (!inner_ret_type->Is<type::Void>()) { | |
| auto attributes = | |
| CloneShaderIOAttributes(func_ast->return_type_attributes, do_interpolate); | |
| // Propagate the non-struct return value as is. | |
| AddOutput("value", func_sem->ReturnType(), func_sem->ReturnLocation(), | |
| std::move(attributes), ctx.dst->Expr(original_result)); | |
| } | |
| } | |
| /// Add a fixed sample mask to the wrapper function output. | |
| /// If there is already a sample mask, bitwise-and it with the fixed mask. | |
| /// Otherwise, create a new output value from the fixed mask. | |
| void AddFixedSampleMask() { | |
| // Check the existing output values for a sample mask builtin. | |
| for (auto& outval : wrapper_output_values) { | |
| if (BuiltinOf(outval.attributes) == builtin::BuiltinValue::kSampleMask) { | |
| // Combine the authored sample mask with the fixed mask. | |
| outval.value = ctx.dst->And(outval.value, u32(cfg.fixed_sample_mask)); | |
| return; | |
| } | |
| } | |
| // No existing sample mask builtin was found, so create a new output value using the fixed | |
| // sample mask. | |
| auto* builtin = ctx.dst->Builtin(builtin::BuiltinValue::kSampleMask); | |
| builtin_attrs.Add(builtin, builtin::BuiltinValue::kSampleMask); | |
| AddOutput("fixed_sample_mask", ctx.dst->create<type::U32>(), std::nullopt, {builtin}, | |
| ctx.dst->Expr(u32(cfg.fixed_sample_mask))); | |
| } | |
| /// Add a point size builtin to the wrapper function output. | |
| void AddVertexPointSize() { | |
| // Create a new output value and assign it a literal 1.0 value. | |
| auto* builtin = ctx.dst->Builtin(builtin::BuiltinValue::kPointSize); | |
| builtin_attrs.Add(builtin, builtin::BuiltinValue::kPointSize); | |
| AddOutput("vertex_point_size", ctx.dst->create<type::F32>(), std::nullopt, {builtin}, | |
| ctx.dst->Expr(1_f)); | |
| } | |
| /// Create an expression for gl_Position.[component] | |
| /// @param component the component of gl_Position to access | |
| /// @returns the new expression | |
| const ast::Expression* GLPosition(const char* component) { | |
| Symbol pos = ctx.dst->Symbols().Register("gl_Position"); | |
| Symbol c = ctx.dst->Symbols().Register(component); | |
| return ctx.dst->MemberAccessor(ctx.dst->Expr(pos), c); | |
| } | |
| /// Comparison function used to reorder struct members such that all members with | |
| /// location attributes appear first (ordered by location slot), followed by | |
| /// those with builtin attributes. | |
| /// @param a a struct member | |
| /// @param b another struct member | |
| /// @returns true if a comes before b | |
| bool StructMemberComparator(const MemberInfo& a, const MemberInfo& b) { | |
| auto* a_loc = ast::GetAttribute<ast::LocationAttribute>(a.member->attributes); | |
| auto* b_loc = ast::GetAttribute<ast::LocationAttribute>(b.member->attributes); | |
| auto* a_blt = ast::GetAttribute<ast::BuiltinAttribute>(a.member->attributes); | |
| auto* b_blt = ast::GetAttribute<ast::BuiltinAttribute>(b.member->attributes); | |
| if (a_loc) { | |
| if (!b_loc) { | |
| // `a` has location attribute and `b` does not: `a` goes first. | |
| return true; | |
| } | |
| // Both have location attributes: smallest goes first. | |
| return a.location < b.location; | |
| } else { | |
| if (b_loc) { | |
| // `b` has location attribute and `a` does not: `b` goes first. | |
| return false; | |
| } | |
| // Both are builtins: order matters for FXC. | |
| auto builtin_a = BuiltinOf(a_blt); | |
| auto builtin_b = BuiltinOf(b_blt); | |
| return BuiltinOrder(builtin_a) < BuiltinOrder(builtin_b); | |
| } | |
| } | |
| /// Create the wrapper function's struct parameter and type objects. | |
| void CreateInputStruct() { | |
| // Sort the struct members to satisfy HLSL interfacing matching rules. | |
| std::sort(wrapper_struct_param_members.begin(), wrapper_struct_param_members.end(), | |
| [&](auto& a, auto& b) { return StructMemberComparator(a, b); }); | |
| utils::Vector<const ast::StructMember*, 8> members; | |
| for (auto& mem : wrapper_struct_param_members) { | |
| members.Push(mem.member); | |
| } | |
| // Create the new struct type. | |
| auto struct_name = ctx.dst->Sym(); | |
| auto* in_struct = ctx.dst->create<ast::Struct>(ctx.dst->Ident(struct_name), | |
| std::move(members), utils::Empty); | |
| ctx.InsertBefore(ctx.src->AST().GlobalDeclarations(), func_ast, in_struct); | |
| // Create a new function parameter using this struct type. | |
| auto* param = ctx.dst->Param(InputStructSymbol(), ctx.dst->ty(struct_name)); | |
| wrapper_ep_parameters.Push(param); | |
| } | |
| /// Create and return the wrapper function's struct result object. | |
| /// @returns the struct type | |
| ast::Struct* CreateOutputStruct() { | |
| utils::Vector<const ast::Statement*, 8> assignments; | |
| auto wrapper_result = ctx.dst->Symbols().New("wrapper_result"); | |
| // Create the struct members and their corresponding assignment statements. | |
| std::unordered_set<std::string> member_names; | |
| for (auto& outval : wrapper_output_values) { | |
| // Use the original output name, unless that is already taken. | |
| Symbol name; | |
| if (member_names.count(outval.name)) { | |
| name = ctx.dst->Symbols().New(outval.name); | |
| } else { | |
| name = ctx.dst->Symbols().Register(outval.name); | |
| } | |
| member_names.insert(name.Name()); | |
| wrapper_struct_output_members.Push({ | |
| ctx.dst->Member(name, outval.type, std::move(outval.attributes)), | |
| outval.location, | |
| }); | |
| assignments.Push( | |
| ctx.dst->Assign(ctx.dst->MemberAccessor(wrapper_result, name), outval.value)); | |
| } | |
| // Sort the struct members to satisfy HLSL interfacing matching rules. | |
| std::sort(wrapper_struct_output_members.begin(), wrapper_struct_output_members.end(), | |
| [&](auto& a, auto& b) { return StructMemberComparator(a, b); }); | |
| utils::Vector<const ast::StructMember*, 8> members; | |
| for (auto& mem : wrapper_struct_output_members) { | |
| members.Push(mem.member); | |
| } | |
| // Create the new struct type. | |
| auto* out_struct = ctx.dst->create<ast::Struct>(ctx.dst->Ident(ctx.dst->Sym()), | |
| std::move(members), utils::Empty); | |
| ctx.InsertBefore(ctx.src->AST().GlobalDeclarations(), func_ast, out_struct); | |
| // Create the output struct object, assign its members, and return it. | |
| auto* result_object = ctx.dst->Var(wrapper_result, ctx.dst->ty(out_struct->name->symbol)); | |
| wrapper_body.Push(ctx.dst->Decl(result_object)); | |
| for (auto* assignment : assignments) { | |
| wrapper_body.Push(assignment); | |
| } | |
| wrapper_body.Push(ctx.dst->Return(wrapper_result)); | |
| return out_struct; | |
| } | |
| /// Create and assign the wrapper function's output variables. | |
| void CreateGlobalOutputVariables() { | |
| for (auto& outval : wrapper_output_values) { | |
| // Disable validation for use of the `output` address space. | |
| auto attributes = std::move(outval.attributes); | |
| attributes.Push(ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace)); | |
| // Create the global variable and assign it the output value. | |
| auto name = ctx.dst->Symbols().New(outval.name); | |
| ast::Type type = outval.type; | |
| const ast::Expression* lhs = ctx.dst->Expr(name); | |
| if (BuiltinOf(attributes) == builtin::BuiltinValue::kSampleMask) { | |
| // Vulkan requires the type of a SampleMask builtin to be an array. | |
| // Declare it as array<u32, 1> and then store to the first element. | |
| type = ctx.dst->ty.array(type, 1_u); | |
| lhs = ctx.dst->IndexAccessor(lhs, 0_i); | |
| } | |
| ctx.dst->GlobalVar(name, type, builtin::AddressSpace::kOut, std::move(attributes)); | |
| wrapper_body.Push(ctx.dst->Assign(lhs, outval.value)); | |
| } | |
| } | |
| // Recreate the original function without entry point attributes and call it. | |
| /// @returns the inner function call expression | |
| const ast::CallExpression* CallInnerFunction() { | |
| Symbol inner_name; | |
| if (cfg.shader_style == ShaderStyle::kGlsl) { | |
| // In GLSL, clone the original entry point name, as the wrapper will be | |
| // called "main". | |
| inner_name = ctx.Clone(func_ast->name->symbol); | |
| } else { | |
| // Add a suffix to the function name, as the wrapper function will take | |
| // the original entry point name. | |
| auto ep_name = func_ast->name->symbol.Name(); | |
| inner_name = ctx.dst->Symbols().New(ep_name + "_inner"); | |
| } | |
| // Clone everything, dropping the function and return type attributes. | |
| // The parameter attributes will have already been stripped during | |
| // processing. | |
| auto* inner_function = | |
| ctx.dst->create<ast::Function>(ctx.dst->Ident(inner_name), ctx.Clone(func_ast->params), | |
| ctx.Clone(func_ast->return_type), | |
| ctx.Clone(func_ast->body), utils::Empty, utils::Empty); | |
| ctx.Replace(func_ast, inner_function); | |
| // Call the function. | |
| return ctx.dst->Call(inner_function->name->symbol, inner_call_parameters); | |
| } | |
| /// Process the entry point function. | |
| void Process() { | |
| bool needs_fixed_sample_mask = false; | |
| bool needs_vertex_point_size = false; | |
| if (func_ast->PipelineStage() == ast::PipelineStage::kFragment && | |
| cfg.fixed_sample_mask != 0xFFFFFFFF) { | |
| needs_fixed_sample_mask = true; | |
| } | |
| if (func_ast->PipelineStage() == ast::PipelineStage::kVertex && | |
| cfg.emit_vertex_point_size) { | |
| needs_vertex_point_size = true; | |
| } | |
| // Exit early if there is no shader IO to handle. | |
| if (func_sem->Parameters().Length() == 0 && func_sem->ReturnType()->Is<type::Void>() && | |
| !needs_fixed_sample_mask && !needs_vertex_point_size && | |
| cfg.shader_style != ShaderStyle::kGlsl) { | |
| return; | |
| } | |
| // Process the entry point parameters, collecting those that need to be | |
| // aggregated into a single structure. | |
| if (!func_sem->Parameters().IsEmpty()) { | |
| for (auto* param : func_sem->Parameters()) { | |
| if (param->Type()->Is<type::Struct>()) { | |
| ProcessStructParameter(param); | |
| } else { | |
| ProcessNonStructParameter(param); | |
| } | |
| } | |
| // Create a structure parameter for the outer entry point if necessary. | |
| if (!wrapper_struct_param_members.IsEmpty()) { | |
| CreateInputStruct(); | |
| } | |
| } | |
| // Recreate the original function and call it. | |
| auto* call_inner = CallInnerFunction(); | |
| // Process the return type, and start building the wrapper function body. | |
| std::function<ast::Type()> wrapper_ret_type = [&] { return ctx.dst->ty.void_(); }; | |
| if (func_sem->ReturnType()->Is<type::Void>()) { | |
| // The function call is just a statement with no result. | |
| wrapper_body.Push(ctx.dst->CallStmt(call_inner)); | |
| } else { | |
| // Capture the result of calling the original function. | |
| auto* inner_result = ctx.dst->Let(ctx.dst->Symbols().New("inner_result"), call_inner); | |
| wrapper_body.Push(ctx.dst->Decl(inner_result)); | |
| // Process the original return type to determine the outputs that the | |
| // outer function needs to produce. | |
| ProcessReturnType(func_sem->ReturnType(), inner_result->name->symbol); | |
| } | |
| // Add a fixed sample mask, if necessary. | |
| if (needs_fixed_sample_mask) { | |
| AddFixedSampleMask(); | |
| } | |
| // Add the pointsize builtin, if necessary. | |
| if (needs_vertex_point_size) { | |
| AddVertexPointSize(); | |
| } | |
| // Produce the entry point outputs, if necessary. | |
| if (!wrapper_output_values.IsEmpty()) { | |
| if (cfg.shader_style == ShaderStyle::kSpirv || cfg.shader_style == ShaderStyle::kGlsl) { | |
| CreateGlobalOutputVariables(); | |
| } else { | |
| auto* output_struct = CreateOutputStruct(); | |
| wrapper_ret_type = [&, output_struct] { | |
| return ctx.dst->ty(output_struct->name->symbol); | |
| }; | |
| } | |
| } | |
| if (cfg.shader_style == ShaderStyle::kGlsl && | |
| func_ast->PipelineStage() == ast::PipelineStage::kVertex) { | |
| auto* pos_y = GLPosition("y"); | |
| auto* negate_pos_y = | |
| ctx.dst->create<ast::UnaryOpExpression>(ast::UnaryOp::kNegation, GLPosition("y")); | |
| wrapper_body.Push(ctx.dst->Assign(pos_y, negate_pos_y)); | |
| auto* two_z = ctx.dst->Mul(ctx.dst->Expr(2_f), GLPosition("z")); | |
| auto* fixed_z = ctx.dst->Sub(two_z, GLPosition("w")); | |
| wrapper_body.Push(ctx.dst->Assign(GLPosition("z"), fixed_z)); | |
| } | |
| // Create the wrapper entry point function. | |
| // For GLSL, use "main", otherwise take the name of the original | |
| // entry point function. | |
| Symbol name; | |
| if (cfg.shader_style == ShaderStyle::kGlsl) { | |
| name = ctx.dst->Symbols().New("main"); | |
| } else { | |
| name = ctx.Clone(func_ast->name->symbol); | |
| } | |
| auto* wrapper_func = ctx.dst->create<ast::Function>( | |
| ctx.dst->Ident(name), wrapper_ep_parameters, ctx.dst->ty(wrapper_ret_type()), | |
| ctx.dst->Block(wrapper_body), ctx.Clone(func_ast->attributes), utils::Empty); | |
| ctx.InsertAfter(ctx.src->AST().GlobalDeclarations(), func_ast, wrapper_func); | |
| } | |
| /// Retrieve the gl_ string corresponding to a builtin. | |
| /// @param builtin the builtin | |
| /// @param stage the current pipeline stage | |
| /// @param address_space the address space (input or output) | |
| /// @returns the gl_ string corresponding to that builtin | |
| const char* GLSLBuiltinToString(builtin::BuiltinValue builtin, | |
| ast::PipelineStage stage, | |
| builtin::AddressSpace address_space) { | |
| switch (builtin) { | |
| case builtin::BuiltinValue::kPosition: | |
| switch (stage) { | |
| case ast::PipelineStage::kVertex: | |
| return "gl_Position"; | |
| case ast::PipelineStage::kFragment: | |
| return "gl_FragCoord"; | |
| default: | |
| return ""; | |
| } | |
| case builtin::BuiltinValue::kVertexIndex: | |
| return "gl_VertexID"; | |
| case builtin::BuiltinValue::kInstanceIndex: | |
| return "gl_InstanceID"; | |
| case builtin::BuiltinValue::kFrontFacing: | |
| return "gl_FrontFacing"; | |
| case builtin::BuiltinValue::kFragDepth: | |
| return "gl_FragDepth"; | |
| case builtin::BuiltinValue::kLocalInvocationId: | |
| return "gl_LocalInvocationID"; | |
| case builtin::BuiltinValue::kLocalInvocationIndex: | |
| return "gl_LocalInvocationIndex"; | |
| case builtin::BuiltinValue::kGlobalInvocationId: | |
| return "gl_GlobalInvocationID"; | |
| case builtin::BuiltinValue::kNumWorkgroups: | |
| return "gl_NumWorkGroups"; | |
| case builtin::BuiltinValue::kWorkgroupId: | |
| return "gl_WorkGroupID"; | |
| case builtin::BuiltinValue::kSampleIndex: | |
| return "gl_SampleID"; | |
| case builtin::BuiltinValue::kSampleMask: | |
| if (address_space == builtin::AddressSpace::kIn) { | |
| return "gl_SampleMaskIn"; | |
| } else { | |
| return "gl_SampleMask"; | |
| } | |
| default: | |
| return ""; | |
| } | |
| } | |
| /// Convert a given GLSL builtin value to the corresponding WGSL value. | |
| /// @param builtin the builtin variable | |
| /// @param value the value to convert | |
| /// @param ast_type (inout) the incoming WGSL and outgoing GLSL types | |
| /// @returns an expression representing the GLSL builtin converted to what | |
| /// WGSL expects | |
| const ast::Expression* FromGLSLBuiltin(builtin::BuiltinValue builtin, | |
| const ast::Expression* value, | |
| ast::Type& ast_type) { | |
| switch (builtin) { | |
| case builtin::BuiltinValue::kVertexIndex: | |
| case builtin::BuiltinValue::kInstanceIndex: | |
| case builtin::BuiltinValue::kSampleIndex: | |
| // GLSL uses i32 for these, so bitcast to u32. | |
| value = ctx.dst->Bitcast(ast_type, value); | |
| ast_type = ctx.dst->ty.i32(); | |
| break; | |
| case builtin::BuiltinValue::kSampleMask: | |
| // gl_SampleMask is an array of i32. Retrieve the first element and | |
| // bitcast it to u32. | |
| value = ctx.dst->IndexAccessor(value, 0_i); | |
| value = ctx.dst->Bitcast(ast_type, value); | |
| ast_type = ctx.dst->ty.array(ctx.dst->ty.i32(), 1_u); | |
| break; | |
| default: | |
| break; | |
| } | |
| return value; | |
| } | |
| /// Convert a given WGSL value to the type expected when assigning to a | |
| /// GLSL builtin. | |
| /// @param builtin the builtin variable | |
| /// @param value the value to convert | |
| /// @param type (out) the type to which the value was converted | |
| /// @returns the converted value which can be assigned to the GLSL builtin | |
| const ast::Expression* ToGLSLBuiltin(builtin::BuiltinValue builtin, | |
| const ast::Expression* value, | |
| const type::Type*& type) { | |
| switch (builtin) { | |
| case builtin::BuiltinValue::kVertexIndex: | |
| case builtin::BuiltinValue::kInstanceIndex: | |
| case builtin::BuiltinValue::kSampleIndex: | |
| case builtin::BuiltinValue::kSampleMask: | |
| type = ctx.dst->create<type::I32>(); | |
| value = ctx.dst->Bitcast(CreateASTTypeFor(ctx, type), value); | |
| break; | |
| default: | |
| break; | |
| } | |
| return value; | |
| } | |
| }; | |
| Transform::ApplyResult CanonicalizeEntryPointIO::Apply(const Program* src, | |
| const DataMap& inputs, | |
| DataMap&) const { | |
| ProgramBuilder b; | |
| CloneContext ctx{&b, src, /* auto_clone_symbols */ true}; | |
| auto* cfg = inputs.Get<Config>(); | |
| if (cfg == nullptr) { | |
| b.Diagnostics().add_error(diag::System::Transform, | |
| "missing transform data for " + std::string(TypeInfo().name)); | |
| return Program(std::move(b)); | |
| } | |
| // Remove entry point IO attributes from struct declarations. | |
| // New structures will be created for each entry point, as necessary. | |
| for (auto* ty : src->AST().TypeDecls()) { | |
| if (auto* struct_ty = ty->As<ast::Struct>()) { | |
| for (auto* member : struct_ty->members) { | |
| for (auto* attr : member->attributes) { | |
| if (IsShaderIOAttribute(attr)) { | |
| ctx.Remove(member->attributes, attr); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| for (auto* func_ast : src->AST().Functions()) { | |
| if (!func_ast->IsEntryPoint()) { | |
| continue; | |
| } | |
| State state(ctx, *cfg, func_ast); | |
| state.Process(); | |
| } | |
| ctx.Clone(); | |
| return Program(std::move(b)); | |
| } | |
| CanonicalizeEntryPointIO::Config::Config(ShaderStyle style, | |
| uint32_t sample_mask, | |
| bool emit_point_size) | |
| : shader_style(style), | |
| fixed_sample_mask(sample_mask), | |
| emit_vertex_point_size(emit_point_size) {} | |
| CanonicalizeEntryPointIO::Config::Config(const Config&) = default; | |
| CanonicalizeEntryPointIO::Config::~Config() = default; | |
| } // namespace tint::transform |