| // Copyright 2023 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| #include "src/tint/lang/spirv/reader/parser/parser.h" | |
| #include <algorithm> | |
| #include <list> | |
| #include <memory> | |
| #include <optional> | |
| #include <span> | |
| #include <string> | |
| #include <unordered_map> | |
| #include <unordered_set> | |
| #include <utility> | |
| #include <vector> | |
| #include "src/tint/utils/ice/ice.h" | |
| #include "src/utils/numeric.h" | |
| #define SPV_ENABLE_UTILITY_CODE | |
| #include "spirv/unified1/spirv.hpp11" | |
| // This header is in an external dependency, so warnings cannot be fixed without upstream changes. | |
| TINT_BEGIN_DISABLE_WARNING(NEWLINE_EOF); | |
| TINT_BEGIN_DISABLE_WARNING(OLD_STYLE_CAST); | |
| TINT_BEGIN_DISABLE_WARNING(SIGN_CONVERSION); | |
| TINT_BEGIN_DISABLE_WARNING(WEAK_VTABLES); | |
| TINT_BEGIN_DISABLE_WARNING(UNSAFE_BUFFER_USAGE); | |
| TINT_BEGIN_DISABLE_WARNING(LIFETIME_SAFETY_INVALIDATION); | |
| #include "source/opt/build_module.h" | |
| #include "source/opt/resolve_binding_conflicts_pass.h" | |
| #include "source/opt/split_combined_image_sampler_pass.h" | |
| TINT_END_DISABLE_WARNING(LIFETIME_SAFETY_INVALIDATION); | |
| TINT_END_DISABLE_WARNING(UNSAFE_BUFFER_USAGE); | |
| TINT_END_DISABLE_WARNING(WEAK_VTABLES); | |
| TINT_END_DISABLE_WARNING(SIGN_CONVERSION); | |
| TINT_END_DISABLE_WARNING(OLD_STYLE_CAST); | |
| TINT_END_DISABLE_WARNING(NEWLINE_EOF); | |
| #include "src/tint/lang/core/ir/builder.h" | |
| #include "src/tint/lang/core/ir/module.h" | |
| #include "src/tint/lang/core/ir/referenced_module_vars.h" | |
| #include "src/tint/lang/core/type/builtin_structs.h" | |
| #include "src/tint/lang/spirv/builtin_fn.h" | |
| #include "src/tint/lang/spirv/ir/builtin_call.h" | |
| #include "src/tint/lang/spirv/type/explicit_layout_array.h" | |
| #include "src/tint/lang/spirv/type/image.h" | |
| #include "src/tint/lang/spirv/type/literal.h" | |
| #include "src/tint/lang/spirv/type/sampled_image.h" | |
| #include "src/tint/lang/spirv/validate/validate.h" | |
| using namespace tint::core::number_suffixes; // NOLINT | |
| using namespace tint::core::fluent_types; // NOLINT | |
| namespace tint::spirv::reader { | |
| namespace { | |
| // Stores information for operands which need to be calculated after a block is complete. Because | |
| // a phi can store values which come after it, we can't calculate the value when the `OpPhi` is | |
| // seen. | |
| struct ReplacementValue { | |
| // The terminator instruction the operand belongs to | |
| core::ir::Terminator* terminator; | |
| // The operand index in `terminator` to replace | |
| uint32_t idx; | |
| // The SPIR-V value id to create the value from | |
| uint32_t value_id; | |
| }; | |
| /// The SPIR-V environment that we validate against. | |
| constexpr auto kTargetEnv = SPV_ENV_VULKAN_1_1; | |
| /// @returns true if the given type is a handle type (image, sampler, or sampled image) | |
| bool IsHandleType(const spvtools::opt::analysis::Type* type) { | |
| return type->AsImage() || type->AsSampler() || type->AsSampledImage(); | |
| } | |
| /// @returns true if the given type is a struct decorated with Block or BufferBlock | |
| bool IsBlockOrBufferBlock(const spvtools::opt::analysis::Type* type) { | |
| if (auto* struct_ty = type->AsStruct()) { | |
| for (const auto& deco : struct_ty->decorations()) { | |
| auto dec = spv::Decoration(deco[0]); | |
| if (dec == spv::Decoration::Block || dec == spv::Decoration::BufferBlock) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| /// PIMPL class for SPIR-V parser. | |
| /// Validates the SPIR-V module and then parses it to produce a Tint IR module. | |
| class Parser { | |
| public: | |
| explicit Parser(const Options& options) : options_(options) {} | |
| ~Parser() = default; | |
| /// @param spirv the SPIR-V binary data | |
| /// @returns the generated SPIR-V IR module on success, or failure | |
| Result<core::ir::Module> Run(std::span<const uint32_t> spirv) { | |
| // Validate the incoming SPIR-V binary. | |
| validate::Options options{ | |
| .target_env = kTargetEnv, | |
| .uniform_buffer_standard_layout = true, | |
| }; | |
| TINT_CHECK_RESULT(validate::Validate(spirv, options)); | |
| // Build the SPIR-V tools internal representation of the SPIR-V module. | |
| spvtools::Context context(kTargetEnv); | |
| spirv_context_ = spvtools::BuildModule(kTargetEnv, context.CContext()->consumer, | |
| spirv.data(), spirv.size()); | |
| if (!spirv_context_) { | |
| return Failure("failed to build the internal representation of the module"); | |
| } | |
| // Run SPIR-V opt transforms to make the input friendlier for the SPIR-V frontend. | |
| { | |
| spvtools::opt::SplitCombinedImageSamplerPass pass; | |
| auto status = pass.Run(spirv_context_.get()); | |
| if (status == spvtools::opt::Pass::Status::Failure) { | |
| return Failure("failed to run SplitCombinedImageSamplerPass in SPIR-V opt"); | |
| } | |
| } | |
| if (options_.sampler_mappings.empty()) { | |
| spvtools::opt::ResolveBindingConflictsPass pass; | |
| auto status = pass.Run(spirv_context_.get()); | |
| if (status == spvtools::opt::Pass::Status::Failure) { | |
| return Failure("failed to run ResolveBindingConflictsPass in SPIR-V opt"); | |
| } | |
| } | |
| // Check for unsupported extensions. | |
| for (const auto& ext : spirv_context_->extensions()) { | |
| auto name = ext.GetOperand(0).AsString(); | |
| if (name != "SPV_KHR_storage_buffer_storage_class" && | |
| name != "SPV_KHR_non_semantic_info" && // | |
| name != "SPV_KHR_16bit_storage" && // | |
| name != "SPV_KHR_terminate_invocation" && // | |
| // TODO(423644565): We assume the barriers are correct. We should check for any | |
| // operation that makes barrier assumptions that aren't consistent with WGSL and | |
| // generate the needed barriers. | |
| name != "SPV_KHR_vulkan_memory_model") { | |
| return Failure("SPIR-V extension '" + name + "' is not supported"); | |
| } | |
| } | |
| // Check for unsupported capabilities. | |
| for (auto it = spirv_context_->capability_begin(); it != spirv_context_->capability_end(); | |
| ++it) { | |
| auto capability = static_cast<spv::Capability>(it->GetSingleWordInOperand(0)); | |
| switch (capability) { | |
| case spv::Capability::ClipDistance: | |
| case spv::Capability::DerivativeControl: | |
| case spv::Capability::Float16: | |
| case spv::Capability::Geometry: | |
| case spv::Capability::GroupNonUniform: | |
| case spv::Capability::GroupNonUniformArithmetic: | |
| case spv::Capability::GroupNonUniformBallot: | |
| case spv::Capability::GroupNonUniformQuad: | |
| case spv::Capability::GroupNonUniformShuffle: | |
| case spv::Capability::GroupNonUniformShuffleRelative: | |
| case spv::Capability::GroupNonUniformVote: | |
| case spv::Capability::Image1D: | |
| case spv::Capability::ImageQuery: | |
| case spv::Capability::InputAttachment: | |
| case spv::Capability::Matrix: | |
| case spv::Capability::Sampled1D: | |
| case spv::Capability::SampledCubeArray: | |
| case spv::Capability::SampleRateShading: | |
| case spv::Capability::Shader: | |
| case spv::Capability::StorageBuffer16BitAccess: | |
| case spv::Capability::StorageImageExtendedFormats: | |
| case spv::Capability::StorageInputOutput16: | |
| case spv::Capability::StoragePushConstant16: | |
| case spv::Capability::UniformAndStorageBuffer16BitAccess: | |
| case spv::Capability::VulkanMemoryModel: | |
| case spv::Capability::VulkanMemoryModelDeviceScope: | |
| // Supported | |
| break; | |
| default: | |
| return Failure("SPIR-V capability '" + | |
| std::string(spv::CapabilityToString(capability)) + | |
| "' is not supported"); | |
| } | |
| } | |
| TINT_CHECK_RESULT(ValidateTypes()); | |
| // Register imported instruction sets | |
| for (const auto& import : spirv_context_->ext_inst_imports()) { | |
| auto name = import.GetInOperand(0).AsString(); | |
| if (name == "GLSL.std.450") { | |
| glsl_std_450_imports_.insert(import.result_id()); | |
| } else if (name.find("NonSemantic.") == 0) { | |
| ignored_imports_.insert(import.result_id()); | |
| } else { | |
| return Failure("Unrecognized extended instruction set: " + name); | |
| } | |
| } | |
| // Set properties that are supported by the SPIR-V parser. | |
| ir_.properties.Add(core::ir::Property::kAllowLocationForNumericComposites); | |
| ir_.properties.Add(core::ir::Property::kAllowMultipleEntryPoints); | |
| ir_.properties.Add(core::ir::Property::kAllowNonCoreTypes); | |
| ir_.properties.Add(core::ir::Property::kAllowOverrides); | |
| ir_.properties.Add(core::ir::Property::kAllowPhonyInstructions); | |
| ir_.properties.Add(core::ir::Property::kAllowPointSizeBuiltin); | |
| ir_.properties.Add(core::ir::Property::kAllowPointerToHandle); | |
| ir_.properties.Add(core::ir::Property::kAllowStructMatrixDecorations); | |
| ir_.properties.Add(core::ir::Property::kAllowUnannotatedModuleIOVariables); | |
| ir_.properties.Add(core::ir::Property::kAllowVectorElementPointer); | |
| ir_.properties.Add(core::ir::Property::kAllowBackendSpecificShaderIO); | |
| ir_.properties.Add(core::ir::Property::kAllow16BitFloats); | |
| RegisterNames(); | |
| id_stack_.emplace_back(); | |
| { | |
| TINT_SCOPED_ASSIGNMENT(current_block_, ir_.root_block); | |
| TINT_CHECK_RESULT(EmitSpecConstants()); | |
| TINT_CHECK_RESULT(EmitModuleScopeVariables()); | |
| } | |
| TINT_CHECK_RESULT(EmitFunctions()); | |
| TINT_CHECK_RESULT(EmitEntryPointAttributes()); | |
| RemapSamplers(); | |
| RemapBufferBlockAddressSpace(); | |
| AddRefToOutputsIfNeeded(); | |
| return std::move(ir_); | |
| } | |
| // If the spir-v struct was marked as `BufferBlock` then we always treat this as a storage | |
| // address space. We do this as a post-pass because we have to propagate the change through all | |
| // the types that derive from the buffer block, e.g. any access needs to change from `Uniform` | |
| // to `Storage` address space. | |
| void RemapBufferBlockAddressSpace() { | |
| for (auto* inst : *ir_.root_block) { | |
| auto* var = inst->As<core::ir::Var>(); | |
| if (!var) { | |
| continue; | |
| } | |
| auto* ptr = var->Result()->Type()->As<core::type::Pointer>(); | |
| TINT_ASSERT(ptr); | |
| auto* str = ptr->UnwrapPtr()->As<core::type::Struct>(); | |
| if (!str) { | |
| continue; | |
| } | |
| if (!storage_buffer_types_.contains(str)) { | |
| continue; | |
| } | |
| auto iter = var_to_original_access_mode_.find(var); | |
| TINT_ASSERT(iter != var_to_original_access_mode_.end()); | |
| auto access_mode = iter->second; | |
| // Handle the case of the struct members all being marked as `NonWritable` | |
| if (consider_non_writable_.contains(str)) { | |
| access_mode = core::Access::kRead; | |
| } | |
| // Storage buffer can not be `write` only | |
| if (access_mode == core::Access::kWrite) { | |
| access_mode = core::Access::kReadWrite; | |
| } | |
| var->Result()->SetType(ty_.ptr(core::AddressSpace::kStorage, str, access_mode)); | |
| UpdateUsagesToStorageAddressSpace(var->Result(), access_mode); | |
| } | |
| } | |
| void UpdateUsagesToStorageAddressSpace(core::ir::Value* val, core::Access access_mode) { | |
| for (auto& usage : val->UsagesUnsorted()) { | |
| if (usage->instruction->Results().IsEmpty()) { | |
| continue; | |
| } | |
| auto* res = usage->instruction->Result(); | |
| auto* ptr = res->Type()->As<core::type::Pointer>(); | |
| if (!ptr) { | |
| continue; | |
| } | |
| TINT_ASSERT(ptr->AddressSpace() == core::AddressSpace::kUniform); | |
| res->SetType(ty_.ptr(core::AddressSpace::kStorage, ptr->StoreType(), access_mode)); | |
| UpdateUsagesToStorageAddressSpace(res, access_mode); | |
| } | |
| } | |
| void AddRefToOutputsIfNeeded() { | |
| for (auto& entry_point : spirv_context_->module()->entry_points()) { | |
| uint32_t spv_id = entry_point.GetSingleWordInOperand(1); | |
| TINT_ASSERT(functions_.Contains(spv_id)); | |
| auto* func = *(functions_.Get(spv_id)); | |
| for (uint32_t i = 3; i < entry_point.NumInOperands(); ++i) { | |
| auto* val = Value(entry_point.GetSingleWordInOperand(i)); | |
| b_.Phony(val)->InsertBefore(func->Block()->Front()); | |
| } | |
| } | |
| } | |
| void RemapSamplers() { | |
| for (auto* inst : *ir_.root_block) { | |
| auto* var = inst->As<core::ir::Var>(); | |
| if (!var) { | |
| continue; | |
| } | |
| auto* ptr = var->Result()->Type()->As<core::type::Pointer>(); | |
| TINT_ASSERT(ptr); | |
| if (!ptr->StoreType()->As<core::type::Sampler>()) { | |
| continue; | |
| } | |
| TINT_ASSERT(var->BindingPoint().has_value()); | |
| auto bp = var->BindingPoint().value(); | |
| auto used = used_bindings.GetOrAddZero(bp); | |
| // Only one use is the sampler itself. | |
| if (used == 1) { | |
| continue; | |
| } | |
| auto& binding = max_binding.GetOrAddZero(bp.group); | |
| binding += 1; | |
| var->SetBindingPoint(bp.group, binding); | |
| } | |
| } | |
| std::optional<uint16_t> GetSpecId(const spvtools::opt::Instruction& inst) { | |
| auto decos = | |
| spirv_context_->get_decoration_mgr()->GetDecorationsFor(inst.result_id(), true); | |
| for (const auto* deco_inst : decos) { | |
| TINT_ASSERT(deco_inst->opcode() == spv::Op::OpDecorate); | |
| if (deco_inst->GetSingleWordInOperand(1) == | |
| static_cast<uint32_t>(spv::Decoration::SpecId)) { | |
| return {static_cast<uint16_t>(deco_inst->GetSingleWordInOperand(2))}; | |
| } | |
| } | |
| return std::nullopt; | |
| } | |
| void CreateOverride(const spvtools::opt::Instruction& inst, | |
| core::ir::Value* value, | |
| std::optional<uint16_t> spec_id) { | |
| auto* override_ = b_.Override(Type(inst.type_id())); | |
| override_->SetInitializer(value); | |
| if (spec_id.has_value()) { | |
| override_->SetOverrideId(OverrideId{spec_id.value()}); | |
| } | |
| Emit(override_, inst.result_id()); | |
| Symbol name = GetSymbolFor(inst.result_id()); | |
| if (name.IsValid()) { | |
| ir_.SetName(override_, name); | |
| } | |
| } | |
| // Generate a module-scope const declaration for each instruction | |
| // that is OpSpecConstantTrue, OpSpecConstantFalse, or OpSpecConstant. | |
| Result<SuccessType> EmitSpecConstants() { | |
| for (auto& inst : spirv_context_->types_values()) { | |
| switch (inst.opcode()) { | |
| case spv::Op::OpSpecConstantTrue: | |
| case spv::Op::OpSpecConstantFalse: { | |
| auto* value = b_.Value(inst.opcode() == spv::Op::OpSpecConstantTrue); | |
| auto spec_id = GetSpecId(inst); | |
| if (spec_id.has_value()) { | |
| CreateOverride(inst, value, spec_id); | |
| } else { | |
| // No spec_id means treat this as a constant. | |
| AddValue(inst.result_id(), value); | |
| } | |
| break; | |
| } | |
| case spv::Op::OpSpecConstant: { | |
| auto literal = inst.GetSingleWordInOperand(0); | |
| auto* ty = spirv_context_->get_type_mgr()->GetType(inst.type_id()); | |
| auto* constant = | |
| spirv_context_->get_constant_mgr()->GetConstant(ty, std::vector{literal}); | |
| core::ir::Value* value = tint::Switch( | |
| Type(spirv_context_->get_type_mgr()->GetId(ty)), // | |
| [&](const core::type::I32*) { | |
| return b_.Constant(i32(constant->GetS32())); | |
| }, | |
| [&](const core::type::U32*) { | |
| return b_.Constant(u32(constant->GetU32())); | |
| }, | |
| [&](const core::type::F32*) { | |
| return b_.Constant(f32(constant->GetFloat())); | |
| }, | |
| [&](const core::type::F16*) { | |
| auto bits = constant->AsScalarConstant()->GetU32BitValue(); | |
| return b_.Constant(f16::FromBits(static_cast<uint16_t>(bits))); | |
| }); | |
| if (!value) { | |
| return Failure("unsupported type for OpSpecConstant: " + ty->str()); | |
| } | |
| auto spec_id = GetSpecId(inst); | |
| CreateOverride(inst, value, spec_id); | |
| break; | |
| } | |
| case spv::Op::OpSpecConstantOp: { | |
| auto op = inst.GetSingleWordInOperand(0); | |
| // Store the name away and remove it from the name list. | |
| // This keeps any `Emit*` call in the switch below for | |
| // gaining the name we want associated to the override. | |
| std::string name; | |
| auto iter = id_to_name_.find(inst.result_id()); | |
| if (iter != id_to_name_.end()) { | |
| name = iter->second; | |
| id_to_name_.erase(inst.result_id()); | |
| } | |
| switch (static_cast<spv::Op>(op)) { | |
| case spv::Op::OpBitwiseAnd: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kBitwiseAnd, 3); | |
| break; | |
| case spv::Op::OpBitwiseOr: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kBitwiseOr, 3); | |
| break; | |
| case spv::Op::OpBitwiseXor: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kBitwiseXor, 3); | |
| break; | |
| case spv::Op::OpIEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kEqual, 3); | |
| break; | |
| case spv::Op::OpINotEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kNotEqual, 3); | |
| break; | |
| case spv::Op::OpSGreaterThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSGreaterThan, 3); | |
| break; | |
| case spv::Op::OpSGreaterThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSGreaterThanEqual, 3); | |
| break; | |
| case spv::Op::OpSLessThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSLessThan, 3); | |
| break; | |
| case spv::Op::OpSLessThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSLessThanEqual, 3); | |
| break; | |
| case spv::Op::OpUGreaterThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kUGreaterThan, 3); | |
| break; | |
| case spv::Op::OpUGreaterThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kUGreaterThanEqual, 3); | |
| break; | |
| case spv::Op::OpULessThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kULessThan, 3); | |
| break; | |
| case spv::Op::OpULessThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kULessThanEqual, 3); | |
| break; | |
| case spv::Op::OpLogicalAnd: | |
| EmitBinary(inst, core::BinaryOp::kAnd, 3); | |
| break; | |
| case spv::Op::OpLogicalOr: | |
| EmitBinary(inst, core::BinaryOp::kOr, 3); | |
| break; | |
| case spv::Op::OpLogicalNot: | |
| EmitUnary(inst, core::UnaryOp::kNot, 3); | |
| break; | |
| case spv::Op::OpLogicalEqual: | |
| EmitBinary(inst, core::BinaryOp::kEqual, 3); | |
| break; | |
| case spv::Op::OpLogicalNotEqual: | |
| EmitBinary(inst, core::BinaryOp::kNotEqual, 3); | |
| break; | |
| case spv::Op::OpNot: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kNot, 3); | |
| break; | |
| case spv::Op::OpSConvert: | |
| return Failure( | |
| "can't translate SConvert: WGSL does not have concrete " | |
| "integer types of different widths"); | |
| case spv::Op::OpUConvert: | |
| return Failure( | |
| "can't translate UConvert: WGSL does not have concrete " | |
| "integer types of different widths"); | |
| case spv::Op::OpFConvert: | |
| EmitOrAdd(b_.Convert(Type(inst.type_id()), | |
| Value(inst.GetSingleWordInOperand(1))), | |
| inst.result_id()); | |
| break; | |
| case spv::Op::OpSNegate: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSNegate, 3); | |
| break; | |
| case spv::Op::OpIAdd: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kAdd, 3); | |
| break; | |
| case spv::Op::OpISub: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSub, 3); | |
| break; | |
| case spv::Op::OpIMul: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kMul, 3); | |
| break; | |
| case spv::Op::OpSDiv: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSDiv, 3); | |
| break; | |
| case spv::Op::OpUDiv: | |
| EmitBinary(inst, core::BinaryOp::kDivide, 3); | |
| break; | |
| case spv::Op::OpUMod: | |
| EmitBinary(inst, core::BinaryOp::kModulo, 3); | |
| break; | |
| case spv::Op::OpSMod: | |
| case spv::Op::OpSRem: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSMod, 3); | |
| break; | |
| case spv::Op::OpShiftLeftLogical: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kShiftLeftLogical, | |
| 3); | |
| break; | |
| case spv::Op::OpShiftRightLogical: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kShiftRightLogical, | |
| 3); | |
| break; | |
| case spv::Op::OpShiftRightArithmetic: | |
| EmitSpirvExplicitBuiltinCall( | |
| inst, spirv::BuiltinFn::kShiftRightArithmetic, 3); | |
| break; | |
| case spv::Op::OpCompositeExtract: | |
| if (!Type(inst.type_id())->IsScalar()) { | |
| return Failure( | |
| "can't translate OpSpecConstantOp with CompositeExtract that " | |
| "returns a composite: OpSpecConstantOp maps to a WGSL override " | |
| "declaration, but WGSL overrides must have scalar type"); | |
| } | |
| EmitCompositeExtract(inst, 3); | |
| break; | |
| case spv::Op::OpCompositeInsert: | |
| return Failure( | |
| "can't translate OpSpecConstantOp with CompositeInsert: " | |
| "OpSpecConstantOp maps to a WGSL override declaration, " | |
| "but WGSL overrides must have scalar type"); | |
| case spv::Op::OpVectorShuffle: | |
| return Failure( | |
| "can't translate OpSpecConstantOp with VectorShuffle: " | |
| "OpSpecConstantOp maps to a WGSL override declaration, " | |
| "but WGSL overrides must have scalar type"); | |
| case spv::Op::OpSelect: | |
| if (!Type(inst.type_id())->IsScalar()) { | |
| return Failure( | |
| "can't translate OpSpecConstantOp with Select that returns " | |
| "a vector: OpSpecConstantOp maps to a WGSL override " | |
| "declaration, " | |
| "but WGSL overrides must have scalar type"); | |
| } | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSelect, 3); | |
| break; | |
| default: | |
| return Failure("Unknown spec constant operation: " + | |
| std::to_string(op)); | |
| } | |
| // Restore the saved name, if any, in order to provide that | |
| // name to the override. | |
| if (!name.empty()) { | |
| id_to_name_.insert({inst.result_id(), name}); | |
| } | |
| CreateOverride(inst, Value(inst.result_id()), std::nullopt); | |
| break; | |
| } | |
| case spv::Op::OpSpecConstantComposite: { | |
| auto spec_id = GetSpecId(inst); | |
| if (spec_id.has_value()) { | |
| return Failure("OpSpecConstantComposite cannot be decorated with SpecId"); | |
| } | |
| auto* cnst = SpvConstant(inst.result_id()); | |
| if (cnst != nullptr) { | |
| // The spec constant is made of literals, so it's return as a constant from | |
| // SPIR-V Tools Opt. We can just ignore it and let the normal constant | |
| // handling take over. | |
| break; | |
| } | |
| Vector<uint32_t, 4> args; | |
| args.Reserve(inst.NumInOperands()); | |
| for (uint32_t i = 0; i < inst.NumInOperands(); ++i) { | |
| uint32_t id = inst.GetSingleWordInOperand(i); | |
| args.Push(id); | |
| } | |
| spec_composites_.insert({inst.result_id(), SpecComposite{ | |
| .type = Type(inst.type_id()), | |
| .args = args, | |
| }}); | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| } | |
| return Success; | |
| } | |
| void RegisterNames() { | |
| // Register names from OpName | |
| for (const auto& inst : spirv_context_->debugs2()) { | |
| switch (inst.opcode()) { | |
| case spv::Op::OpName: { | |
| const auto name = inst.GetInOperand(1).AsString(); | |
| if (!name.empty()) { | |
| id_to_name_[inst.GetSingleWordInOperand(0)] = name; | |
| } | |
| break; | |
| } | |
| case spv::Op::OpMemberName: { | |
| const auto name = inst.GetInOperand(2).AsString(); | |
| if (!name.empty()) { | |
| uint32_t struct_id = inst.GetSingleWordInOperand(0); | |
| uint32_t member_idx = inst.GetSingleWordInOperand(1); | |
| auto iter = struct_to_member_names_.insert({struct_id, {}}); | |
| auto& members = (*(iter.first)).second; | |
| if (members.size() < (member_idx + 1)) { | |
| members.resize(member_idx + 1); | |
| } | |
| members[member_idx] = name; | |
| } | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| /// @param sc a SPIR-V storage class | |
| /// @returns the Tint address space for a SPIR-V storage class | |
| core::AddressSpace AddressSpace(spv::StorageClass sc) { | |
| switch (sc) { | |
| case spv::StorageClass::Input: | |
| return core::AddressSpace::kIn; | |
| case spv::StorageClass::Output: | |
| return core::AddressSpace::kOut; | |
| case spv::StorageClass::Function: | |
| return core::AddressSpace::kFunction; | |
| case spv::StorageClass::Private: | |
| return core::AddressSpace::kPrivate; | |
| case spv::StorageClass::StorageBuffer: | |
| return core::AddressSpace::kStorage; | |
| case spv::StorageClass::Uniform: | |
| return core::AddressSpace::kUniform; | |
| case spv::StorageClass::UniformConstant: | |
| return core::AddressSpace::kHandle; | |
| case spv::StorageClass::Workgroup: | |
| return core::AddressSpace::kWorkgroup; | |
| case spv::StorageClass::PushConstant: | |
| return core::AddressSpace::kImmediate; | |
| default: | |
| TINT_UNREACHABLE() | |
| << "unhandled SPIR-V storage class: " << spv::StorageClassToString(sc); | |
| } | |
| } | |
| /// @param b a SPIR-V BuiltIn | |
| /// @returns the Tint builtin value for a SPIR-V BuiltIn decoration | |
| Result<core::BuiltinValue> Builtin(spv::BuiltIn b) { | |
| switch (b) { | |
| case spv::BuiltIn::FragCoord: | |
| return core::BuiltinValue::kPosition; | |
| case spv::BuiltIn::FragDepth: | |
| return core::BuiltinValue::kFragDepth; | |
| case spv::BuiltIn::FrontFacing: | |
| return core::BuiltinValue::kFrontFacing; | |
| case spv::BuiltIn::GlobalInvocationId: | |
| return core::BuiltinValue::kGlobalInvocationId; | |
| case spv::BuiltIn::InstanceIndex: | |
| return core::BuiltinValue::kInstanceIndex; | |
| case spv::BuiltIn::LocalInvocationId: | |
| return core::BuiltinValue::kLocalInvocationId; | |
| case spv::BuiltIn::LocalInvocationIndex: | |
| return core::BuiltinValue::kLocalInvocationIndex; | |
| case spv::BuiltIn::NumWorkgroups: | |
| return core::BuiltinValue::kNumWorkgroups; | |
| case spv::BuiltIn::PointSize: | |
| return core::BuiltinValue::kPointSize; | |
| case spv::BuiltIn::Position: | |
| return core::BuiltinValue::kPosition; | |
| case spv::BuiltIn::SampleId: | |
| return core::BuiltinValue::kSampleIndex; | |
| case spv::BuiltIn::SampleMask: | |
| return core::BuiltinValue::kSampleMask; | |
| case spv::BuiltIn::SubgroupId: | |
| return core::BuiltinValue::kSubgroupId; | |
| case spv::BuiltIn::SubgroupSize: | |
| return core::BuiltinValue::kSubgroupSize; | |
| case spv::BuiltIn::SubgroupLocalInvocationId: | |
| return core::BuiltinValue::kSubgroupInvocationId; | |
| case spv::BuiltIn::NumSubgroups: | |
| return core::BuiltinValue::kNumSubgroups; | |
| case spv::BuiltIn::VertexIndex: | |
| return core::BuiltinValue::kVertexIndex; | |
| case spv::BuiltIn::WorkgroupId: | |
| return core::BuiltinValue::kWorkgroupId; | |
| case spv::BuiltIn::ClipDistance: | |
| return core::BuiltinValue::kClipDistances; | |
| case spv::BuiltIn::CullDistance: | |
| return core::BuiltinValue::kCullDistance; | |
| case spv::BuiltIn::PrimitiveId: | |
| return core::BuiltinValue::kPrimitiveIndex; | |
| default: | |
| return Failure("unhandled SPIR-V BuiltIn: " + std::string(spv::BuiltInToString(b)) + | |
| " (val = " + std::to_string(static_cast<uint32_t>(b)) + ")"); | |
| } | |
| } | |
| /// @param id a SPIR-V result ID for a type declaration instruction | |
| /// @param access_mode an optional access mode (for pointers) | |
| /// @returns a Tint type object | |
| const core::type::Type* Type(uint32_t id, core::Access access_mode = core::Access::kUndefined) { | |
| auto* type = spirv_context_->get_type_mgr()->GetType(id); | |
| // Only use the access mode for the map key if it is used as part of the type in Tint IR. | |
| auto key_mode = core::Access::kUndefined; | |
| if (type->kind() == spvtools::opt::analysis::Type::kImage || | |
| type->kind() == spvtools::opt::analysis::Type::kPointer) { | |
| key_mode = access_mode; | |
| } | |
| return types_.GetOrAdd(TypeKey{id, key_mode}, [&]() -> const core::type::Type* { | |
| uint32_t array_stride = 0; | |
| bool set_as_storage_buffer = false; | |
| for (auto& deco : type->decorations()) { | |
| switch (spv::Decoration(deco[0])) { | |
| case spv::Decoration::Block: { | |
| // Ignore, just means it's a memory block. | |
| break; | |
| } | |
| case spv::Decoration::BufferBlock: { | |
| set_as_storage_buffer = true; | |
| break; | |
| } | |
| case spv::Decoration::ArrayStride: { | |
| array_stride = deco[1]; | |
| break; | |
| } | |
| default: { | |
| TINT_UNREACHABLE() << "unhandled type decoration " << deco[0]; | |
| } | |
| } | |
| } | |
| // Storage buffer is only set ons structs | |
| if (set_as_storage_buffer) { | |
| TINT_ASSERT(type->kind() == spvtools::opt::analysis::Type::kStruct); | |
| } | |
| // ArrayStride is only handled on the array type for now | |
| if (array_stride > 0) { | |
| if (type->kind() != spvtools::opt::analysis::Type::kArray && | |
| type->kind() != spvtools::opt::analysis::Type::kRuntimeArray) { | |
| TINT_UNREACHABLE() | |
| << "ArrayStride is only accepted on an Array or RuntimeArray"; | |
| } | |
| } | |
| switch (type->kind()) { | |
| case spvtools::opt::analysis::Type::kVoid: { | |
| return ty_.void_(); | |
| } | |
| case spvtools::opt::analysis::Type::kBool: { | |
| return ty_.bool_(); | |
| } | |
| case spvtools::opt::analysis::Type::kInteger: { | |
| auto* int_ty = type->AsInteger(); | |
| TINT_ASSERT(int_ty->width() == 32); | |
| if (int_ty->IsSigned()) { | |
| return ty_.i32(); | |
| } else { | |
| return ty_.u32(); | |
| } | |
| } | |
| case spvtools::opt::analysis::Type::kFloat: { | |
| auto* float_ty = type->AsFloat(); | |
| if (float_ty->width() == 16) { | |
| return ty_.f16(); | |
| } else if (float_ty->width() == 32) { | |
| return ty_.f32(); | |
| } else { | |
| TINT_UNREACHABLE() | |
| << "unsupported floating point type width: " << float_ty->width(); | |
| } | |
| } | |
| case spvtools::opt::analysis::Type::kVector: { | |
| auto* vec_ty = type->AsVector(); | |
| TINT_ASSERT(vec_ty->element_count() <= 4); | |
| return ty_.vec( | |
| Type(spirv_context_->get_type_mgr()->GetId(vec_ty->element_type())), | |
| vec_ty->element_count()); | |
| } | |
| case spvtools::opt::analysis::Type::kMatrix: { | |
| auto* mat_ty = type->AsMatrix(); | |
| TINT_ASSERT(mat_ty->element_count() <= 4); | |
| return ty_.mat( | |
| As<core::type::Vector>( | |
| Type(spirv_context_->get_type_mgr()->GetId(mat_ty->element_type()))), | |
| mat_ty->element_count()); | |
| } | |
| case spvtools::opt::analysis::Type::kArray: { | |
| return EmitArray(id, array_stride); | |
| } | |
| case spvtools::opt::analysis::Type::kRuntimeArray: { | |
| return EmitRuntimeArray(id, array_stride); | |
| } | |
| case spvtools::opt::analysis::Type::kStruct: { | |
| const core::type::Struct* str_ty = EmitStruct(id); | |
| if (set_as_storage_buffer) { | |
| storage_buffer_types_.insert(str_ty); | |
| } | |
| return str_ty; | |
| } | |
| case spvtools::opt::analysis::Type::kPointer: { | |
| return EmitPointer(id, access_mode); | |
| } | |
| case spvtools::opt::analysis::Type::kSampler: { | |
| return ty_.sampler(); | |
| } | |
| case spvtools::opt::analysis::Type::kImage: { | |
| auto* img = type->AsImage(); | |
| auto* sampled_ty = | |
| Type(spirv_context_->get_type_mgr()->GetId(img->sampled_type())); | |
| auto dim = static_cast<type::Dim>(img->dim()); | |
| auto depth = static_cast<type::Depth>(img->depth()); | |
| auto arrayed = | |
| img->is_arrayed() ? type::Arrayed::kArrayed : type::Arrayed::kNonArrayed; | |
| auto ms = img->is_multisampled() ? type::Multisampled::kMultisampled | |
| : type::Multisampled::kSingleSampled; | |
| auto sampled = static_cast<type::Sampled>(img->sampled()); | |
| auto texel_format = ToTexelFormat(img->format()).Get(); | |
| // If the access mode is undefined then default to read/write for the image | |
| access_mode = access_mode == core::Access::kUndefined ? core::Access::kReadWrite | |
| : access_mode; | |
| TINT_ASSERT(img->dim() == spv::Dim::Dim1D || img->dim() == spv::Dim::Dim2D || | |
| img->dim() == spv::Dim::Dim3D || img->dim() == spv::Dim::Cube || | |
| img->dim() == spv::Dim::SubpassData); | |
| TINT_ASSERT(img->sampled() != 0); | |
| TINT_ASSERT(depth != type::Depth::kDepth || sampled_ty->Is<core::type::F32>()); | |
| return ty_.Get<spirv::type::Image>(sampled_ty, dim, depth, arrayed, ms, sampled, | |
| texel_format, access_mode); | |
| } | |
| case spvtools::opt::analysis::Type::kSampledImage: { | |
| auto* sampled = type->AsSampledImage(); | |
| return ty_.Get<spirv::type::SampledImage>( | |
| Type(spirv_context_->get_type_mgr()->GetId(sampled->image_type()))); | |
| } | |
| default: { | |
| TINT_UNREACHABLE() << "unhandled SPIR-V type: " << type->str(); | |
| } | |
| } | |
| }); | |
| } | |
| Result<core::TexelFormat> ToTexelFormat(spv::ImageFormat fmt) { | |
| switch (fmt) { | |
| case spv::ImageFormat::Unknown: | |
| return core::TexelFormat::kUndefined; | |
| // 8 bit channels | |
| case spv::ImageFormat::Rgba8: | |
| return core::TexelFormat::kRgba8Unorm; | |
| case spv::ImageFormat::Rgba8Snorm: | |
| return core::TexelFormat::kRgba8Snorm; | |
| case spv::ImageFormat::Rgba8ui: | |
| return core::TexelFormat::kRgba8Uint; | |
| case spv::ImageFormat::Rgba8i: | |
| return core::TexelFormat::kRgba8Sint; | |
| case spv::ImageFormat::R8: | |
| return core::TexelFormat::kR8Unorm; | |
| case spv::ImageFormat::R8i: | |
| return core::TexelFormat::kR8Sint; | |
| case spv::ImageFormat::R8ui: | |
| return core::TexelFormat::kR8Uint; | |
| case spv::ImageFormat::R8Snorm: | |
| return core::TexelFormat::kR8Snorm; | |
| case spv::ImageFormat::Rg8: | |
| return core::TexelFormat::kRg8Unorm; | |
| case spv::ImageFormat::Rg8Snorm: | |
| return core::TexelFormat::kRg8Snorm; | |
| case spv::ImageFormat::Rg8i: | |
| return core::TexelFormat::kRg8Sint; | |
| case spv::ImageFormat::Rg8ui: | |
| return core::TexelFormat::kRg8Uint; | |
| // 16 bit channels | |
| case spv::ImageFormat::R16: | |
| return core::TexelFormat::kR16Unorm; | |
| case spv::ImageFormat::R16f: | |
| return core::TexelFormat::kR16Float; | |
| case spv::ImageFormat::R16i: | |
| return core::TexelFormat::kR16Sint; | |
| case spv::ImageFormat::R16ui: | |
| return core::TexelFormat::kR16Uint; | |
| case spv::ImageFormat::R16Snorm: | |
| return core::TexelFormat::kR16Snorm; | |
| case spv::ImageFormat::Rgba16: | |
| return core::TexelFormat::kRgba16Unorm; | |
| case spv::ImageFormat::Rgba16ui: | |
| return core::TexelFormat::kRgba16Uint; | |
| case spv::ImageFormat::Rgba16i: | |
| return core::TexelFormat::kRgba16Sint; | |
| case spv::ImageFormat::Rgba16f: | |
| return core::TexelFormat::kRgba16Float; | |
| case spv::ImageFormat::Rgba16Snorm: | |
| return core::TexelFormat::kRgba16Snorm; | |
| case spv::ImageFormat::Rg16: | |
| return core::TexelFormat::kRg16Unorm; | |
| case spv::ImageFormat::Rg16i: | |
| return core::TexelFormat::kRg16Sint; | |
| case spv::ImageFormat::Rg16ui: | |
| return core::TexelFormat::kRg16Uint; | |
| case spv::ImageFormat::Rg16f: | |
| return core::TexelFormat::kRg16Float; | |
| case spv::ImageFormat::Rg16Snorm: | |
| return core::TexelFormat::kRg16Snorm; | |
| // 32 bit channels | |
| case spv::ImageFormat::R32ui: | |
| return core::TexelFormat::kR32Uint; | |
| case spv::ImageFormat::R32i: | |
| return core::TexelFormat::kR32Sint; | |
| case spv::ImageFormat::R32f: | |
| return core::TexelFormat::kR32Float; | |
| case spv::ImageFormat::Rg32ui: | |
| return core::TexelFormat::kRg32Uint; | |
| case spv::ImageFormat::Rg32i: | |
| return core::TexelFormat::kRg32Sint; | |
| case spv::ImageFormat::Rg32f: | |
| return core::TexelFormat::kRg32Float; | |
| case spv::ImageFormat::Rgba32ui: | |
| return core::TexelFormat::kRgba32Uint; | |
| case spv::ImageFormat::Rgba32i: | |
| return core::TexelFormat::kRgba32Sint; | |
| case spv::ImageFormat::Rgba32f: | |
| return core::TexelFormat::kRgba32Float; | |
| case spv::ImageFormat::Rgb10A2: | |
| return core::TexelFormat::kRgb10A2Unorm; | |
| case spv::ImageFormat::Rgb10a2ui: | |
| return core::TexelFormat::kRgb10A2Uint; | |
| case spv::ImageFormat::R11fG11fB10f: | |
| return core::TexelFormat::kRg11B10Ufloat; | |
| default: | |
| break; | |
| } | |
| return Failure("invalid image format: " + std::to_string(dawn::to_underlying(fmt))); | |
| } | |
| /// Validates that all types and their decorations in the SPIR-V module can be handled by Tint. | |
| /// @returns Success, or a Failure describing the unsupported type or decoration | |
| Result<SuccessType> ValidateTypes() { | |
| for (const auto& type : *spirv_context_->get_type_mgr()) { | |
| const auto* ty = type.second; | |
| // Validate type-level decorations. | |
| for (const auto& deco : ty->decorations()) { | |
| switch (spv::Decoration(deco[0])) { | |
| case spv::Decoration::Block: | |
| case spv::Decoration::BufferBlock: | |
| case spv::Decoration::ArrayStride: | |
| break; | |
| default: | |
| return Failure( | |
| "unhandled type decoration: " + | |
| std::string(spv::DecorationToString(spv::Decoration(deco[0])))); | |
| } | |
| } | |
| switch (ty->kind()) { | |
| case spvtools::opt::analysis::Type::kVoid: | |
| case spvtools::opt::analysis::Type::kBool: | |
| case spvtools::opt::analysis::Type::kFloat: | |
| case spvtools::opt::analysis::Type::kVector: | |
| case spvtools::opt::analysis::Type::kMatrix: | |
| case spvtools::opt::analysis::Type::kFunction: | |
| case spvtools::opt::analysis::Type::kSampler: | |
| case spvtools::opt::analysis::Type::kSampledImage: | |
| break; | |
| case spvtools::opt::analysis::Type::kInteger: { | |
| auto* int_ty = ty->AsInteger(); | |
| if (int_ty->width() != 32) { | |
| return Failure("unsupported integer width: " + | |
| std::to_string(int_ty->width())); | |
| } | |
| break; | |
| } | |
| case spvtools::opt::analysis::Type::kArray: { | |
| auto* arr_ty = ty->AsArray(); | |
| if (IsHandleType(arr_ty->element_type())) { | |
| return Failure("arrays of handle types are not supported"); | |
| } | |
| if (IsBlockOrBufferBlock(arr_ty->element_type())) { | |
| return Failure("arrays of buffer types are not supported"); | |
| } | |
| const auto& length = arr_ty->length_info(); | |
| if (length.words.empty() || | |
| length.words[0] != spvtools::opt::analysis::Array::LengthInfo::kConstant) { | |
| return Failure("specialized array lengths are not supported"); | |
| } | |
| break; | |
| } | |
| case spvtools::opt::analysis::Type::kRuntimeArray: { | |
| auto* arr_ty = ty->AsRuntimeArray(); | |
| if (IsHandleType(arr_ty->element_type())) { | |
| return Failure("arrays of handle types are not supported"); | |
| } | |
| if (IsBlockOrBufferBlock(arr_ty->element_type())) { | |
| return Failure("arrays of buffer types are not supported"); | |
| } | |
| break; | |
| } | |
| case spvtools::opt::analysis::Type::kStruct: { | |
| auto* struct_ty = ty->AsStruct(); | |
| if (struct_ty->element_types().empty()) { | |
| return Failure("empty structures are not supported"); | |
| } | |
| for (const auto& [member_idx, decos] : struct_ty->element_decorations()) { | |
| for (const auto& deco : decos) { | |
| switch (spv::Decoration(deco[0])) { | |
| case spv::Decoration::NonWritable: | |
| case spv::Decoration::ColMajor: | |
| case spv::Decoration::NonReadable: | |
| case spv::Decoration::RelaxedPrecision: | |
| case spv::Decoration::Coherent: | |
| case spv::Decoration::Restrict: | |
| case spv::Decoration::RowMajor: | |
| case spv::Decoration::Offset: | |
| case spv::Decoration::MatrixStride: | |
| case spv::Decoration::Invariant: | |
| case spv::Decoration::Location: | |
| case spv::Decoration::NoPerspective: | |
| case spv::Decoration::Flat: | |
| case spv::Decoration::Centroid: | |
| case spv::Decoration::Sample: | |
| break; | |
| case spv::Decoration::BuiltIn: { | |
| auto builtin_res = Builtin(spv::BuiltIn(deco[1])); | |
| if (builtin_res != Success) { | |
| return builtin_res.Failure(); | |
| } | |
| break; | |
| } | |
| default: | |
| return Failure("unhandled member decoration: " + | |
| std::string(spv::DecorationToString( | |
| spv::Decoration(deco[0])))); | |
| } | |
| } | |
| } | |
| break; | |
| } | |
| case spvtools::opt::analysis::Type::kPointer: { | |
| auto* ptr_ty = ty->AsPointer(); | |
| switch (ptr_ty->storage_class()) { | |
| case spv::StorageClass::Input: | |
| case spv::StorageClass::Output: | |
| case spv::StorageClass::Function: | |
| case spv::StorageClass::Private: | |
| case spv::StorageClass::StorageBuffer: | |
| case spv::StorageClass::Uniform: | |
| case spv::StorageClass::UniformConstant: | |
| case spv::StorageClass::Workgroup: | |
| case spv::StorageClass::PushConstant: | |
| break; | |
| default: | |
| return Failure( | |
| "unhandled SPIR-V storage class: " + | |
| std::string(spv::StorageClassToString(ptr_ty->storage_class()))); | |
| } | |
| break; | |
| } | |
| case spvtools::opt::analysis::Type::kImage: { | |
| auto* img = ty->AsImage(); | |
| if (img->depth() == 1) { | |
| auto* s_ty = img->sampled_type(); | |
| if (!s_ty->AsFloat() || s_ty->AsFloat()->width() != 32) { | |
| return Failure("depth texture sampled type must be f32"); | |
| } | |
| } | |
| auto fmt_res = ToTexelFormat(img->format()); | |
| if (fmt_res != Success) { | |
| return fmt_res.Failure(); | |
| } | |
| break; | |
| } | |
| default: | |
| return Failure("unhandled SPIR-V type: " + ty->str()); | |
| } | |
| } | |
| return Success; | |
| } | |
| /// @param type_id the pointer result_id | |
| /// @param access_mode the access mode | |
| /// @returns a Tint pointer object | |
| const core::type::Type* EmitPointer(uint32_t type_id, core::Access access_mode) { | |
| auto* inst = spirv_context_->get_def_use_mgr()->GetDef(type_id); | |
| auto* subtype = Type(inst->GetInOperand(1).AsId(), access_mode); | |
| // In a few cases we need to adjust the access mode. | |
| // | |
| // 1. Handle is always a read pointer | |
| // 2. If the SPIR-V type should be considered NonWritable | |
| if (subtype->IsHandle() || consider_non_writable_.contains(subtype)) { | |
| access_mode = core::Access::kRead; | |
| } | |
| // Using the deduplicated pointer should be fine because the storage classes must have | |
| // matched if it was a duplicate. | |
| auto* ptr_ty = spirv_context_->get_type_mgr()->GetType(type_id)->AsPointer(); | |
| return ty_.ptr(AddressSpace(ptr_ty->storage_class()), subtype, access_mode); | |
| } | |
| /// @param type_id the spirv result_id | |
| /// @returns a Tint runtime array object | |
| const core::type::Type* EmitRuntimeArray(uint32_t type_id, uint32_t array_stride) { | |
| auto* inst = spirv_context_->get_def_use_mgr()->GetDef(type_id); | |
| auto* elem_ty = Type(inst->GetInOperand(0).AsId()); | |
| uint32_t implicit_stride = tint::RoundUp(elem_ty->Align(), elem_ty->Size()); | |
| if (array_stride == 0 || array_stride == implicit_stride) { | |
| return ty_.runtime_array(elem_ty); | |
| } | |
| return ty_.Get<spirv::type::ExplicitLayoutArray>( | |
| elem_ty, ty_.Get<core::type::RuntimeArrayCount>(), static_cast<uint32_t>(array_stride), | |
| array_stride); | |
| } | |
| /// @param type_id the spirv result_id for the type | |
| /// @param array_stride the stride | |
| /// @returns a Tint array object | |
| const core::type::Type* EmitArray(uint32_t type_id, uint32_t array_stride) { | |
| auto* inst = spirv_context_->get_def_use_mgr()->GetDef(type_id); | |
| // Even deduplicated the length info must match, so safe to use the type manager info here. | |
| auto* arr_ty = spirv_context_->get_type_mgr()->GetType(type_id)->AsArray(); | |
| const auto& length = arr_ty->length_info(); | |
| TINT_ASSERT(!length.words.empty()); | |
| TINT_ASSERT(length.words[0] == spvtools::opt::analysis::Array::LengthInfo::kConstant); | |
| // Get the value from the constant used for the element count. | |
| const auto* count_const = | |
| spirv_context_->get_constant_mgr()->FindDeclaredConstant(length.id); | |
| TINT_ASSERT(count_const); | |
| const uint64_t count_val = count_const->GetZeroExtendedValue(); | |
| TINT_ASSERT(count_val <= UINT32_MAX); | |
| auto* elem_ty = Type(inst->GetInOperand(0).AsId()); | |
| uint32_t implicit_stride = tint::RoundUp(elem_ty->Align(), elem_ty->Size()); | |
| if (array_stride == 0 || array_stride == implicit_stride) { | |
| return ty_.array(elem_ty, static_cast<uint32_t>(count_val)); | |
| } | |
| return ty_.Get<spirv::type::ExplicitLayoutArray>( | |
| elem_ty, ty_.Get<core::type::ConstantArrayCount>(static_cast<uint32_t>(count_val)), | |
| static_cast<uint32_t>(array_stride * count_val), array_stride); | |
| } | |
| /// Calculate the size of a struct member type that has a matrix stride decoration. | |
| uint32_t GetSizeOfTypeWithMatrixStride(const core::type::Type* type, | |
| uint32_t stride, | |
| bool is_row_major) { | |
| return tint::Switch( | |
| type, | |
| [&](const core::type::Matrix* mat) { | |
| return stride * (is_row_major ? mat->Rows() : mat->Columns()); | |
| }, | |
| [&](const core::type::Array* arr) { | |
| auto size = GetSizeOfTypeWithMatrixStride(arr->ElemType(), stride, is_row_major); | |
| // The size of a runtime-sized array is the size of a single element, so we only | |
| // have to handle the fixed-sized array case here. | |
| if (auto count = arr->ConstantCount()) { | |
| size *= count.value(); | |
| } | |
| return size; | |
| }, | |
| TINT_ICE_ON_NO_MATCH); | |
| } | |
| /// @param type_id the struct type id | |
| /// @returns a Tint struct object | |
| const core::type::Struct* EmitStruct(uint32_t type_id) { | |
| auto* inst = spirv_context_->get_def_use_mgr()->GetDef(type_id); | |
| uint32_t member_count = inst->NumInOperandWords(); | |
| TINT_ASSERT(member_count != 0) << "empty structures are not supported"; | |
| std::vector<std::string>* member_names = nullptr; | |
| auto struct_to_member_iter = struct_to_member_names_.find(type_id); | |
| if (struct_to_member_iter != struct_to_member_names_.end()) { | |
| member_names = &((*struct_to_member_iter).second); | |
| } | |
| // We can use the struct type for the decorations, since even if it was deduplicated then | |
| // the decorations must be the same. | |
| auto* struct_ty = spirv_context_->get_type_mgr()->GetType(type_id)->AsStruct(); | |
| // Build a list of struct members. | |
| uint32_t current_size = 0u; | |
| uint32_t non_writable_members = 0; | |
| Vector<core::type::StructMember*, 4> members; | |
| for (uint32_t i = 0; i < member_count; i++) { | |
| auto* member_ty = Type(inst->GetInOperand(i).AsId()); | |
| uint32_t align = std::max<uint32_t>(member_ty->Align(), 1u); | |
| uint32_t offset = tint::RoundUp(align, current_size); | |
| core::IOAttributes attributes; | |
| auto interpolation = [&]() -> core::Interpolation& { | |
| // Create the interpolation field with the default values on first call. | |
| if (!attributes.interpolation.has_value()) { | |
| attributes.interpolation = | |
| core::Interpolation{core::InterpolationType::kPerspective, | |
| core::InterpolationSampling::kUndefined}; | |
| } | |
| return attributes.interpolation.value(); | |
| }; | |
| bool is_row_major = false; | |
| uint32_t matrix_stride = 0; | |
| // Handle member decorations that affect layout or attributes. | |
| if (struct_ty->element_decorations().count(i)) { | |
| for (auto& deco : struct_ty->element_decorations().at(i)) { | |
| switch (spv::Decoration(deco[0])) { | |
| case spv::Decoration::NonWritable: | |
| // WGSL doesn't have a non-writable attribute on struct members, but, if | |
| // the SPIR-V structure has NonWritable on all members then we treat the | |
| // entire structure as non-writable. | |
| non_writable_members += 1; | |
| break; | |
| case spv::Decoration::ColMajor: // Do nothing, WGSL is column major | |
| case spv::Decoration::NonReadable: // Not supported in WGSL | |
| case spv::Decoration::RelaxedPrecision: // Not supported in WGSL | |
| break; | |
| case spv::Decoration::Coherent: | |
| // Tint has coherent memory semantics, so this is a no-op. | |
| break; | |
| case spv::Decoration::Restrict: | |
| // All Tint pointers are restrict. This is a no-op. | |
| break; | |
| case spv::Decoration::RowMajor: | |
| is_row_major = true; | |
| break; | |
| case spv::Decoration::MatrixStride: | |
| matrix_stride = deco[1]; | |
| break; | |
| case spv::Decoration::Offset: | |
| offset = deco[1]; | |
| break; | |
| case spv::Decoration::BuiltIn: { | |
| auto builtin_res = Builtin(spv::BuiltIn(deco[1])); | |
| TINT_ASSERT(builtin_res == Success); | |
| attributes.builtin = builtin_res.Get(); | |
| break; | |
| } | |
| case spv::Decoration::Invariant: | |
| attributes.invariant = true; | |
| break; | |
| case spv::Decoration::Location: | |
| attributes.location = deco[1]; | |
| break; | |
| case spv::Decoration::NoPerspective: | |
| interpolation().type = core::InterpolationType::kLinear; | |
| break; | |
| case spv::Decoration::Flat: | |
| interpolation().type = core::InterpolationType::kFlat; | |
| break; | |
| case spv::Decoration::Centroid: | |
| interpolation().sampling = core::InterpolationSampling::kCentroid; | |
| break; | |
| case spv::Decoration::Sample: | |
| interpolation().sampling = core::InterpolationSampling::kSample; | |
| break; | |
| default: | |
| TINT_UNREACHABLE() << "unhandled member decoration: " << deco[0]; | |
| } | |
| } | |
| } | |
| if (member_ty->IsIntegerScalarOrVector() && attributes.location.has_value()) { | |
| interpolation().type = core::InterpolationType::kFlat; | |
| } | |
| Symbol name; | |
| if (member_names && member_names->size() > i) { | |
| auto n = (*member_names)[i]; | |
| if (!n.empty()) { | |
| name = ir_.symbols.Register(n); | |
| } | |
| } | |
| if (!name.IsValid()) { | |
| name = ir_.symbols.New(); | |
| } | |
| uint32_t size = member_ty->Size(); | |
| if (matrix_stride > 0) { | |
| size = GetSizeOfTypeWithMatrixStride(member_ty, matrix_stride, is_row_major); | |
| } | |
| core::type::StructMember* member = ty_.Get<core::type::StructMember>( | |
| name, member_ty, i, offset, align, size, std::move(attributes)); | |
| if (is_row_major) { | |
| member->SetRowMajor(); | |
| } | |
| if (matrix_stride > 0) { | |
| member->SetMatrixStride(matrix_stride); | |
| } | |
| members.Push(member); | |
| current_size = offset + size; | |
| } | |
| Symbol name = GetUniqueSymbolFor(type_id); | |
| if (!name.IsValid()) { | |
| name = ir_.symbols.New(); | |
| } | |
| auto* strct = ty_.Struct(name, std::move(members)); | |
| if (non_writable_members == member_count) { | |
| consider_non_writable_.insert(strct); | |
| } | |
| return strct; | |
| } | |
| Symbol GetUniqueSymbolFor(uint32_t id) { | |
| auto iter = id_to_name_.find(id); | |
| if (iter != id_to_name_.end()) { | |
| return ir_.symbols.New(iter->second); | |
| } | |
| return Symbol{}; | |
| } | |
| Symbol GetSymbolFor(uint32_t id) { | |
| auto iter = id_to_name_.find(id); | |
| if (iter != id_to_name_.end()) { | |
| return ir_.symbols.Register(iter->second); | |
| } | |
| return Symbol{}; | |
| } | |
| /// @param id a SPIR-V result ID for a function declaration instruction | |
| /// @returns a Tint function object | |
| core::ir::Function* Function(uint32_t id) { | |
| return functions_.GetOrAdd(id, [&] { return b_.Function(ty_.void_()); }); | |
| } | |
| // Passes a value up through control flow to make it visible in an outer scope. Because SPIR-V | |
| // allows an id to be referenced as long as it's dominated, you can access a variable which is | |
| // defined inside an if branch for example. In order for that to be accessed in IR, we have to | |
| // propagate the value as a return of the control instruction (like the if). | |
| // | |
| // e.g. if the IR is similar to the following: | |
| // ``` | |
| // if (b) { | |
| // %a:i32 = let 4; | |
| // exit_if | |
| // } | |
| // %c:i32 = %a + %a; | |
| // ``` | |
| // | |
| // We propagate to something like: | |
| // ``` | |
| // %d:i32 = if (b) { | |
| // %a:i32 = let 4; // The spir-v ID refers to %a at this point | |
| // exit_if %a | |
| // } | |
| // %c:i32 = %d + %d // The spir-v ID will now refer to %d instead of %a | |
| // ``` | |
| // | |
| // We can end up propagating up through multiple levels, so we can end up with something like: | |
| // ``` | |
| // %k:i32 = if (true) { | |
| // %l:i32 = if (false) { | |
| // %m:i32 = if (true) { | |
| // %n:i32 = switch 4 { | |
| // default: { | |
| // %o:i32 = loop { | |
| // %a:i32 = let 4; | |
| // exit_loop %a | |
| // } | |
| // exit_switch %o | |
| // } | |
| // } | |
| // exit_if %n | |
| // } | |
| // exit_if %m | |
| // } | |
| // exit_if %l | |
| // } | |
| // %b:i32 = %k + %k | |
| // ``` | |
| // | |
| // @param id the spir-v ID to propagate up | |
| // @param src the source value being propagated | |
| core::ir::Value* Propagate(uint32_t id, core::ir::Value* src) { | |
| auto* blk = tint::Switch( | |
| src, // | |
| [&](core::ir::BlockParam* bp) { return bp->Block(); }, | |
| [&](core::ir::InstructionResult* res) { return res->Instruction()->Block(); }, | |
| TINT_ICE_ON_NO_MATCH); | |
| // Walk up the set of control instructions from the current `blk`. We'll update the `src` | |
| // instruction as the new result which is to be used for the given SPIR-V `id`. At each | |
| // control instruction we'll add the current `src` as a result of each exit from the control | |
| // instruction, making a new result which is available in the parent scope. | |
| while (blk) { | |
| if (InBlock(blk)) { | |
| break; | |
| } | |
| core::ir::ControlInstruction* ctrl = nullptr; | |
| if (auto* mb = blk->As<core::ir::MultiInBlock>()) { | |
| ctrl = mb->Parent()->As<core::ir::ControlInstruction>(); | |
| TINT_ASSERT(ctrl); | |
| for (auto exit : ctrl->Exits()) { | |
| tint::Switch( | |
| exit.Value(), // | |
| [&](core::ir::ExitLoop* el) { el->PushOperand(src); }, | |
| [&](core::ir::BreakIf* bi) { bi->PushOperand(src); }, // | |
| TINT_ICE_ON_NO_MATCH); | |
| } | |
| } else { | |
| TINT_ASSERT(blk->Terminator()); | |
| ctrl = tint::Switch( | |
| blk->Terminator(), // | |
| [&](core::ir::ExitIf* ei) { return ei->If(); }, | |
| [&](core::ir::ExitSwitch* es) { return es->Switch(); }, | |
| [&](core::ir::ExitLoop* el) { return el->Loop(); }, | |
| [&](core::ir::NextIteration* ni) { | |
| ni->PushOperand(src); | |
| return nullptr; | |
| }, | |
| [&](core::ir::Continue* cont) { | |
| // The propagation is going through a `continue`. This means | |
| // this is the only path to the continuing block, but it also | |
| // means we're current in the continuing block. We can't do | |
| // normal propagation here, we have to pass a block param | |
| // instead. | |
| auto* param = b_.BlockParam(src->Type()); | |
| // We're in the continuing block, so make the block param available in the | |
| // scope. | |
| id_stack_.back().insert(id); | |
| auto* loop = cont->Loop(); | |
| loop->Continuing()->AddParam(param); | |
| cont->PushOperand(src); | |
| // Set `src` as the `param` so it's returned as the new value | |
| src = param; | |
| return nullptr; | |
| }, // | |
| [&](core::ir::Unreachable*) { return blk->Parent(); }, // | |
| TINT_ICE_ON_NO_MATCH); | |
| if (!ctrl) { | |
| break; | |
| } | |
| for (auto& exit : ctrl->Exits()) { | |
| exit->PushOperand(src); | |
| } | |
| } | |
| // If this control has no exits, then we don't need to add the result through the | |
| // control as we're jumping over the control to its parent control. (This is an | |
| // `if` inside a `loop` where the `if` is doing a `break`). | |
| if (ctrl->Exits().IsEmpty()) { | |
| if (ctrl->Is<core::ir::Loop>()) { | |
| TINT_UNREACHABLE() | |
| << "loop detected with no exits which means it's an infinite loop. " | |
| "Infinite loops are not supported in WGSL"; | |
| } | |
| TINT_ASSERT(ctrl->Is<core::ir::If>()); | |
| blk = ctrl->Block(); | |
| continue; | |
| } | |
| // Add a new result to the control instruction | |
| ctrl->AddResult(b_.InstructionResult(src->Type())); | |
| // The source instruction is now the control result we just inserted | |
| src = ctrl->Results().Back(); | |
| // The SPIR-V ID now refers to the propagated value. | |
| values_.Replace(id, src); | |
| blk = ctrl->Block(); | |
| } | |
| return src; | |
| } | |
| // Returns true if the `blk` is the `parent_blk` or any of the ancestors of parent_blk | |
| bool IsContainedInBlock(core::ir::Block* parent_blk, core::ir::Block* blk) { | |
| while (parent_blk) { | |
| if (parent_blk == blk) { | |
| return true; | |
| } | |
| if (!parent_blk->Parent()) { | |
| break; | |
| } | |
| parent_blk = parent_blk->Parent()->Block(); | |
| } | |
| return false; | |
| } | |
| // Propagate a value out of a control instruction. This isn't the same as the other | |
| // propagate as we only need to pull it out through a single blocks exit for the control | |
| // instruction. | |
| core::ir::Value* PropagateTerm(core::ir::Terminator* term, | |
| uint32_t spv_id_being_propagated, | |
| core::ir::Value* source_value, | |
| core::ir::Block* source_block) { | |
| auto* current_ctrl = source_block->Parent(); | |
| core::ir::Block* current_blk = source_block; | |
| core::ir::Value* propagated_value = source_value; | |
| auto* type = source_value->Type(); | |
| while (true) { | |
| if (IsContainedInBlock(term->Block(), current_blk)) { | |
| break; | |
| } | |
| TINT_ASSERT(current_ctrl); | |
| // If the terminator is an exit for this control instruction, we're done as the caller | |
| // will handle adding the result. | |
| bool done = false; | |
| for (auto exit : current_ctrl->Exits()) { | |
| if (exit.Value() == term) { | |
| done = true; | |
| break; | |
| } | |
| } | |
| if (done) { | |
| break; | |
| } | |
| auto* res = b_.InstructionResult(type); | |
| current_ctrl->AddResult(res); | |
| for (auto exit : current_ctrl->Exits()) { | |
| if (IsContainedInBlock(exit->Block(), current_blk)) { | |
| exit->PushOperand(propagated_value); | |
| continue; | |
| } | |
| // Special handling for loop-continuing blocks as the body is visible to the | |
| // continuing so any block params from the body can pass through the exit. | |
| if (auto* loop = current_ctrl->As<core::ir::Loop>()) { | |
| if (exit->Block() == loop->Continuing() && | |
| propagated_value->Is<core::ir::BlockParam>()) { | |
| exit->PushOperand(propagated_value); | |
| } | |
| } | |
| } | |
| propagated_value = res; | |
| current_blk = current_ctrl->Block(); | |
| current_ctrl = current_blk->Parent(); | |
| } | |
| values_.Replace(spv_id_being_propagated, propagated_value); | |
| return propagated_value; | |
| } | |
| // Returns true if the value is a constant value | |
| bool IdIsConstant(uint32_t id) { return SpvConstant(id) || spec_composites_.contains(id); } | |
| // Returns true if this value is currently in scope | |
| bool IdIsInScope(uint32_t id) { | |
| for (auto iter = id_stack_.rbegin(); iter != id_stack_.rend(); ++iter) { | |
| if (iter->contains(id)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Get the spirv constant for the given `id`. `nullptr` if no constant exists. | |
| const spvtools::opt::analysis::Constant* SpvConstant(uint32_t id) { | |
| return spirv_context_->get_constant_mgr()->FindDeclaredConstant(id); | |
| } | |
| /// Attempts to retrieve the current Tint IR value for `id`. This ignores scoping for the | |
| /// variable, if it exists it's returned (or if it's constant it's created). The value will not | |
| /// propagate up through control instructions. | |
| /// | |
| /// @param id a SPIR-V result ID | |
| /// @returns a Tint value object | |
| std::optional<core::ir::Value*> ValueNoPropagate(uint32_t id) { | |
| auto v = values_.Get(id); | |
| if (v) { | |
| return *v; | |
| } | |
| const spvtools::opt::Instruction* inst = spirv_context_->get_def_use_mgr()->GetDef(id); | |
| const core::type::Type* inst_ty = Type(inst->type_id()); | |
| if (const spvtools::opt::analysis::Constant* c = SpvConstant(id)) { | |
| core::ir::Constant* val = b_.Constant(Constant(c)); | |
| // SPIR-V opt will deduplicate structs with the same shape. We handle this in the type | |
| // code below by using the specific instructions type, not the spirv-opt type. But, when | |
| // we retrieve constants, then opt might have used the other type for the struct type. | |
| // So, check the instructions type, if it doesn't match what opt returns, then we | |
| // rebuild the struct below with our corrected type. | |
| if (val->Type() == inst_ty) { | |
| values_.Add(id, val); | |
| return val; | |
| } | |
| } | |
| // In the case of a deduplicated structure, we may need to re-create a null constant | |
| if (inst->opcode() == spv::Op::OpConstantNull) { | |
| TINT_ASSERT(inst_ty->Is<core::type::Struct>()); | |
| core::ir::Value* val = b_.Zero(inst_ty); | |
| values_.Add(id, val); | |
| return val; | |
| } | |
| // If we didn't have a value already, and this instruction is a constructed constant, then | |
| // do the construction. | |
| if (inst->opcode() == spv::Op::OpConstantComposite) { | |
| Vector<const core::constant::Value*, 4> args; | |
| args.Reserve(inst->NumInOperands()); | |
| for (uint32_t i = 0; i < inst->NumInOperands(); ++i) { | |
| auto* cnst = Value(inst->GetSingleWordInOperand(i))->As<core::ir::Constant>(); | |
| TINT_ASSERT(cnst); | |
| args.Push(cnst->Value()); | |
| } | |
| auto* composite = b_.Composite(inst_ty, args); | |
| AddValue(id, composite); | |
| return composite; | |
| } | |
| // If this was a spec composite, then it currently isn't in scope, so we construct | |
| // a new copy and assign the constant ID to the new construct in this scope. | |
| auto iter = spec_composites_.find(id); | |
| if (iter != spec_composites_.end()) { | |
| Vector<core::ir::Value*, 4> args; | |
| for (auto arg : iter->second.args) { | |
| args.Push(Value(arg)); | |
| } | |
| core::ir::Value* construct = nullptr; | |
| b_.Append(current_block_, [&] { construct = b_.Construct(iter->second.type, args); }); | |
| AddValue(id, construct); | |
| return construct; | |
| } | |
| return std::nullopt; | |
| } | |
| /// Attempts to retrieve the current Tint IR value for `id`. If the value exists and is not in | |
| /// scope it will propagate the value up through the control instructions. | |
| /// | |
| /// @param id a SPIR-V result ID | |
| /// @returns a Tint value object | |
| core::ir::Value* Value(uint32_t id, bool add_to_scope = true) { | |
| auto v = ValueNoPropagate(id); | |
| TINT_ASSERT(v.has_value()); | |
| if (v.value()->Is<core::ir::Constant>() || v.value()->Is<core::ir::FunctionParam>() || | |
| IdIsInScope(id)) { | |
| return v.value(); | |
| } | |
| // A return values from a control instruction cannot be a pointer type | |
| // without supporting variable pointers. We regenerate the value so it | |
| // is in scope. | |
| if (v.value()->Type()->Is<core::type::Pointer>()) { | |
| return ReplicatePointer(id); | |
| } | |
| auto* new_v = Propagate(id, v.value()); | |
| if (add_to_scope) { | |
| AddValue(id, new_v); | |
| } | |
| return new_v; | |
| } | |
| /// Replicates a pointer instruction that is defined inside a control flow construct | |
| /// but used outside of it. | |
| /// @param id the SPIR-V result ID of the pointer instruction | |
| /// @returns the replicated Tint IR value | |
| core::ir::Value* ReplicatePointer(uint32_t id) { | |
| auto* spv_inst = spirv_context_->get_def_use_mgr()->GetDef(id); | |
| TINT_ASSERT(spv_inst); | |
| switch (spv_inst->opcode()) { | |
| case spv::Op::OpAccessChain: | |
| case spv::Op::OpInBoundsAccessChain: | |
| EmitAccess(*spv_inst); | |
| break; | |
| case spv::Op::OpCopyObject: | |
| EmitCopyObject(*spv_inst); | |
| break; | |
| default: | |
| TINT_UNREACHABLE() << "unhandled instruction for pointer replication: " | |
| << spv::OpToString(spv_inst->opcode()); | |
| } | |
| auto new_v = values_.Get(id); | |
| TINT_ASSERT(new_v); | |
| return *new_v; | |
| } | |
| /// Creates the Tint IR constant for the SPIR-V `constant` value. | |
| /// | |
| /// @param constant a SPIR-V constant object | |
| /// @returns a Tint constant value | |
| const core::constant::Value* Constant(const spvtools::opt::analysis::Constant* constant) { | |
| // Handle OpConstantNull for all types. | |
| if (constant->AsNullConstant()) { | |
| return ir_.constant_values.Zero( | |
| Type(spirv_context_->get_type_mgr()->GetId(constant->type()))); | |
| } | |
| if (auto* bool_ = constant->AsBoolConstant()) { | |
| return b_.ConstantValue(bool_->value()); | |
| } | |
| if (auto* i = constant->AsIntConstant()) { | |
| auto* int_ty = i->type()->AsInteger(); | |
| TINT_ASSERT(int_ty->width() == 32); | |
| if (int_ty->IsSigned()) { | |
| return b_.ConstantValue(i32(i->GetS32BitValue())); | |
| } else { | |
| return b_.ConstantValue(u32(i->GetU32BitValue())); | |
| } | |
| } | |
| if (auto* f = constant->AsFloatConstant()) { | |
| auto* float_ty = f->type()->AsFloat(); | |
| if (float_ty->width() == 16) { | |
| return b_.ConstantValue(f16::FromBits(static_cast<uint16_t>(f->words()[0]))); | |
| } else if (float_ty->width() == 32) { | |
| return b_.ConstantValue(f32(f->GetFloat())); | |
| } else { | |
| TINT_UNREACHABLE() | |
| << "unsupported floating point type width: " << float_ty->width(); | |
| } | |
| } | |
| if (auto* v = constant->AsVectorConstant()) { | |
| Vector<const core::constant::Value*, 4> elements; | |
| for (auto& el : v->GetComponents()) { | |
| elements.Push(Constant(el)); | |
| } | |
| return ir_.constant_values.Composite( | |
| Type(spirv_context_->get_type_mgr()->GetId(v->type())), std::move(elements)); | |
| } | |
| if (auto* m = constant->AsMatrixConstant()) { | |
| Vector<const core::constant::Value*, 4> columns; | |
| for (auto& el : m->GetComponents()) { | |
| columns.Push(Constant(el)); | |
| } | |
| return ir_.constant_values.Composite( | |
| Type(spirv_context_->get_type_mgr()->GetId(m->type())), std::move(columns)); | |
| } | |
| if (auto* a = constant->AsArrayConstant()) { | |
| Vector<const core::constant::Value*, 16> elements; | |
| for (auto& el : a->GetComponents()) { | |
| elements.Push(Constant(el)); | |
| } | |
| return ir_.constant_values.Composite( | |
| Type(spirv_context_->get_type_mgr()->GetId(a->type())), std::move(elements)); | |
| } | |
| if (auto* s = constant->AsStructConstant()) { | |
| Vector<const core::constant::Value*, 16> elements; | |
| for (auto& el : s->GetComponents()) { | |
| elements.Push(Constant(el)); | |
| } | |
| return ir_.constant_values.Composite( | |
| Type(spirv_context_->get_type_mgr()->GetId(s->type())), std::move(elements)); | |
| } | |
| TINT_ICE() << "unhandled constant type: " << constant->type()->str(); | |
| } | |
| /// @returns a literal operand with the given value | |
| core::ir::Value* Literal(uint32_t value) { | |
| return b_.Constant(ir_.constant_values.Get<core::constant::Scalar<u32>>( | |
| ty_.Get<type::Literal>(), u32(value))); | |
| } | |
| /// Register an IR value for a SPIR-V result ID. | |
| /// @param result_id the SPIR-V result ID | |
| /// @param value the IR value | |
| void AddValue(uint32_t result_id, core::ir::Value* value) { | |
| id_stack_.back().insert(result_id); | |
| values_.Replace(result_id, value); | |
| } | |
| /// Emit an instruction to the current block and associates the result to | |
| /// the spirv result id. | |
| /// @param inst the instruction to emit | |
| /// @param result_id the SPIR-V result ID to register the instruction result for | |
| void Emit(core::ir::Instruction* inst, uint32_t result_id) { | |
| current_block_->Append(inst); | |
| TINT_ASSERT(inst->Results().Length() == 1u); | |
| AddValue(result_id, inst->Result()); | |
| Symbol name = GetSymbolFor(result_id); | |
| if (name.IsValid() && !inst->Result()->Type()->Is<core::type::Void>()) { | |
| ir_.SetName(inst, name); | |
| } | |
| } | |
| /// Emit an instruction to the current block. | |
| /// @param value the value to emit | |
| void EmitWithoutSpvResult(core::ir::Value* value) { | |
| if (auto* inst = value->AsInstruction()) { | |
| current_block_->Append(inst); | |
| TINT_ASSERT(inst->Results().Length() == 1u); | |
| } | |
| } | |
| /// Emit an instruction to the current block. | |
| /// @param value the value to emit | |
| void EmitWithoutResult(core::ir::Instruction* inst) { | |
| TINT_ASSERT(inst->Results().IsEmpty()); | |
| current_block_->Append(inst); | |
| } | |
| /// Emit the module-scope variables. | |
| Result<SuccessType> EmitModuleScopeVariables() { | |
| for (auto& inst : spirv_context_->module()->types_values()) { | |
| switch (inst.opcode()) { | |
| case spv::Op::OpVariable: | |
| TINT_CHECK_RESULT(EmitVar(inst)); | |
| break; | |
| case spv::Op::OpUndef: { | |
| auto* ty = Type(inst.type_id()); | |
| if (ty->Is<core::type::MemoryView>()) { | |
| return Failure("cannot create an undef memory view in WGSL"); | |
| } | |
| AddValue(inst.result_id(), b_.Zero(ty)); | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| } | |
| return Success; | |
| } | |
| /// Emit the functions. | |
| Result<SuccessType> EmitFunctions() { | |
| // Add all the functions in a first pass and then fill in the function bodies. This means | |
| // the function will exist fixing an issues where calling a function that hasn't been seen | |
| // generates the wrong signature. | |
| for (auto& func : *spirv_context_->module()) { | |
| current_spirv_function_ = &func; | |
| Vector<core::ir::FunctionParam*, 4> params; | |
| func.ForEachParam([&](spvtools::opt::Instruction* spirv_param) { | |
| auto* param = b_.FunctionParam(Type(spirv_param->type_id())); | |
| values_.Add(spirv_param->result_id(), param); | |
| Symbol name = GetSymbolFor(spirv_param->result_id()); | |
| if (name.IsValid()) { | |
| ir_.SetName(param, name); | |
| } | |
| params.Push(param); | |
| }); | |
| current_function_ = Function(func.result_id()); | |
| current_function_->SetParams(std::move(params)); | |
| current_function_->SetReturnType(Type(func.type_id())); | |
| Symbol name = GetSymbolFor(func.result_id()); | |
| if (name.IsValid()) { | |
| ir_.SetName(current_function_, name); | |
| } | |
| functions_.Add(func.result_id(), current_function_); | |
| current_spirv_function_ = nullptr; | |
| } | |
| for (auto& func : *spirv_context_->module()) { | |
| current_spirv_function_ = &func; | |
| current_function_ = Function(func.result_id()); | |
| TINT_CHECK_RESULT(EmitBlockParent(current_function_->Block(), *func.entry())); | |
| // No terminator was emitted, that means then end of block is | |
| // unreachable. Mark as such. | |
| if (!current_function_->Block()->Terminator()) { | |
| current_function_->Block()->Append(b_.Unreachable()); | |
| } | |
| current_spirv_function_ = nullptr; | |
| } | |
| return Success; | |
| } | |
| /// Emit entry point attributes. | |
| Result<SuccessType> EmitEntryPointAttributes() { | |
| // Handle OpEntryPoint declarations. | |
| for (auto& entry_point : spirv_context_->module()->entry_points()) { | |
| auto model = entry_point.GetSingleWordInOperand(0); | |
| auto* func = Function(entry_point.GetSingleWordInOperand(1)); | |
| // Set the pipeline stage. | |
| switch (spv::ExecutionModel(model)) { | |
| case spv::ExecutionModel::GLCompute: | |
| func->SetStage(core::ir::Function::PipelineStage::kCompute); | |
| break; | |
| case spv::ExecutionModel::Fragment: | |
| func->SetStage(core::ir::Function::PipelineStage::kFragment); | |
| break; | |
| case spv::ExecutionModel::Vertex: | |
| func->SetStage(core::ir::Function::PipelineStage::kVertex); | |
| break; | |
| default: | |
| return Failure("unhandled execution model: " + std::to_string(model)); | |
| } | |
| // Set the entry point name. | |
| ir_.SetName(func, entry_point.GetOperand(2).AsString()); | |
| if (func->IsCompute()) { | |
| // Search for `WorkgroupSize` decorated Ids | |
| for (const spvtools::opt::Instruction& inst : | |
| spirv_context_->module()->annotations()) { | |
| if (inst.opcode() != spv::Op::OpDecorate || | |
| inst.GetSingleWordInOperand(1) != | |
| dawn::to_underlying(spv::Decoration::BuiltIn) || | |
| inst.GetSingleWordInOperand(2) != | |
| dawn::to_underlying(spv::BuiltIn::WorkgroupSize)) { | |
| continue; | |
| } | |
| uint32_t id = inst.GetSingleWordInOperand(0); | |
| Vector<core::ir::Value*, 3> args; | |
| if (auto* c = SpvConstant(id)) { | |
| auto* vals = c->AsVectorConstant(); | |
| TINT_ASSERT(vals); | |
| for (auto& el : vals->GetComponents()) { | |
| args.Push(b_.Constant(Constant(el))); | |
| } | |
| } else { | |
| TINT_ASSERT(spec_composites_.contains(id)); | |
| auto info = spec_composites_[id]; | |
| TINT_ASSERT(info.args.Length() == 3); | |
| for (auto arg : info.args) { | |
| args.Push(Value(arg)); | |
| } | |
| } | |
| func->SetWorkgroupSize(args[0], args[1], args[2]); | |
| break; | |
| } | |
| } | |
| } | |
| // Handle OpExecutionMode declarations. | |
| for (auto& execution_mode : spirv_context_->module()->execution_modes()) { | |
| auto* func = functions_.GetOr(execution_mode.GetSingleWordInOperand(0), nullptr); | |
| auto mode = execution_mode.GetSingleWordInOperand(1); | |
| TINT_ASSERT(func); | |
| switch (spv::ExecutionMode(mode)) { | |
| case spv::ExecutionMode::LocalSize: | |
| func->SetWorkgroupSize( | |
| b_.Constant(u32(execution_mode.GetSingleWordInOperand(2))), | |
| b_.Constant(u32(execution_mode.GetSingleWordInOperand(3))), | |
| b_.Constant(u32(execution_mode.GetSingleWordInOperand(4)))); | |
| break; | |
| case spv::ExecutionMode::DepthGreater: | |
| return Failure("ExecutionMode DepthGreater is not supported in WGSL"); | |
| case spv::ExecutionMode::DepthLess: | |
| return Failure("ExecutionMode DepthLess is not supported in WGSL"); | |
| case spv::ExecutionMode::DepthUnchanged: | |
| return Failure("ExecutionMode DepthUnchanged is not supported in WGSL"); | |
| case spv::ExecutionMode::EarlyFragmentTests: | |
| return Failure("ExecutionMode EarlyFragmentTests is not supported in WGSL"); | |
| case spv::ExecutionMode::DepthReplacing: | |
| case spv::ExecutionMode::OriginUpperLeft: | |
| // These are ignored as they are implicitly supported by Tint IR. | |
| break; | |
| default: | |
| return Failure("unhandled execution mode: " + std::to_string(mode)); | |
| } | |
| } | |
| return Success; | |
| } | |
| bool InBlock(core::ir::Block* blk) { return current_blocks_.contains(blk); } | |
| // A block parent is a container for a scope, like a `{}`d section in code. It controls the | |
| // block addition to the current blocks and the ID stack entry for the block. | |
| Result<SuccessType> EmitBlockParent(core::ir::Block* dst, spvtools::opt::BasicBlock& src) { | |
| TINT_ASSERT(!InBlock(dst)); | |
| id_stack_.emplace_back(); | |
| current_blocks_.insert(dst); | |
| auto res = EmitBlock(dst, src); | |
| current_blocks_.erase(dst); | |
| id_stack_.pop_back(); | |
| return res; | |
| } | |
| /// Emit the contents of SPIR-V block @p src into Tint IR block @p dst. | |
| /// @param dst the Tint IR block to append to | |
| /// @param src the SPIR-V block to emit | |
| Result<SuccessType> EmitBlock(core::ir::Block* dst, spvtools::opt::BasicBlock& src) { | |
| TINT_SCOPED_ASSIGNMENT(current_block_, dst); | |
| // Register the merge if this is a header block | |
| auto* merge_inst = src.GetMergeInst(); | |
| if (merge_inst) { | |
| auto merge_id = merge_inst->GetSingleWordInOperand(0); | |
| spirv_merge_id_to_header_id_.insert({merge_id, src.id()}); | |
| } | |
| values_to_replace_.push_back({}); | |
| // If this is a loop merge block, so we need to treat it as a Loop. | |
| auto* loop_merge_inst = src.GetLoopMergeInst(); | |
| core::ir::Loop* loop = nullptr; | |
| if (loop_merge_inst) { | |
| // Emit the loop instruction into the current block. | |
| EmitLoop(src); | |
| // The loop header is a walk stop block, which was created in the emit loop method. Get | |
| // the loop back so we can change the current insertion block. | |
| loop = StopWalkingAt(src.id())->As<core::ir::Loop>(); | |
| TINT_ASSERT(loop); | |
| id_stack_.emplace_back(); | |
| current_blocks_.insert(loop->Body()); | |
| // Now emit the remainder of the block into the loop body. We do this now so the loop | |
| // emitted above is in the parent block. | |
| current_block_ = loop->Body(); | |
| } | |
| // Note, this comes after the loop code since the current block is set to the loop body | |
| spirv_id_to_block_.insert({src.id(), current_block_}); | |
| TINT_CHECK_RESULT(ProcessInstructions(src)); | |
| // Add the body terminator if necessary | |
| if (loop && !loop->Body()->Terminator()) { | |
| loop->Body()->Append(b_.Unreachable()); | |
| } | |
| // For any `OpPhi` values we saw, insert their `Value` now. We do this at the end of the | |
| // processing of instructions because a phi can refer to instructions defined after it | |
| // in the block. | |
| auto replace = values_to_replace_.back(); | |
| for (auto& val : replace) { | |
| auto v = ValueNoPropagate(val.value_id); | |
| TINT_ASSERT(v.has_value()); | |
| val.terminator->SetOperand(val.idx, v.value()); | |
| } | |
| values_to_replace_.pop_back(); | |
| if (!loop) { | |
| return Success; | |
| } | |
| // Emit the continuing block. The continue block is within the scope of the body block, | |
| // so we don't pop the id stack yet. | |
| auto continue_id = loop_merge_inst->GetSingleWordInOperand(1); | |
| // Capture any continue PHi header values before emitting the continue because the value | |
| // will get re-written to be the continue block param, so we have to capture first. | |
| auto continue_loop_phi_ids = block_phi_values_[continue_id]; | |
| std::vector<core::ir::Value*> continue_loop_phi_values; | |
| if (!continue_loop_phi_ids.empty()) { | |
| continue_loop_phi_values.reserve(continue_loop_phi_ids.size()); | |
| for (auto& id : continue_loop_phi_ids) { | |
| auto v = ValueNoPropagate(id); | |
| if (v.has_value()) { | |
| continue_loop_phi_values.push_back(v.value()); | |
| } | |
| } | |
| } | |
| TINT_CHECK_RESULT(EmitContinueBlock(src.id(), continue_id, loop)); | |
| // Remove the body block id stack before emitting the merge block. | |
| current_blocks_.erase(loop->Body()); | |
| id_stack_.pop_back(); | |
| // If we added phi's to the continuing block, we may have exits from the body which | |
| // aren't valid. | |
| if (loop->Continuing()->Params().Length() > 0) { | |
| for (auto incoming : loop->Continuing()->InboundSiblingBranches()) { | |
| TINT_ASSERT(incoming->Is<core::ir::Continue>()); | |
| if (incoming->Args().size() == loop->Continuing()->Params().Length()) { | |
| continue; | |
| } | |
| // Check if the block this instruction exists in has default phi result that we | |
| // can append. | |
| auto inst_to_blk_iter = inst_to_spirv_block_.find(incoming); | |
| if (inst_to_blk_iter != inst_to_spirv_block_.end()) { | |
| uint32_t spirv_blk = inst_to_blk_iter->second; | |
| auto phi_values_from_loop_header = block_phi_values_[spirv_blk]; | |
| // We require a PHI value, but the current block doesn't have any from the | |
| // header. In this case, check the `continue` target to see if it has header | |
| // provided phi values. If so, use that one. | |
| if (phi_values_from_loop_header.empty()) { | |
| TINT_ASSERT(!continue_loop_phi_values.empty()); | |
| for (auto* value : continue_loop_phi_values) { | |
| incoming->PushOperand(value); | |
| } | |
| } else { | |
| // If there were phi values, push them to this instruction | |
| for (auto value_id : phi_values_from_loop_header) { | |
| auto* value = Value(value_id); | |
| incoming->PushOperand(value); | |
| } | |
| } | |
| } else { | |
| TINT_UNREACHABLE() << "unable to provide needed continue arguments"; | |
| } | |
| } | |
| } | |
| // Emit the merge block | |
| auto merge_id = loop_merge_inst->GetSingleWordInOperand(0); | |
| const auto& merge_bb = current_spirv_function_->FindBlock(merge_id); | |
| TINT_CHECK_RESULT(EmitBlock(dst, *merge_bb)); | |
| return Success; | |
| } | |
| Result<SuccessType> EmitContinueBlock(uint32_t src_id, | |
| uint32_t continue_id, | |
| core::ir::Loop* loop) { | |
| // We're emitting the continue block, so remove it from the continue targets as it can no | |
| // longer be a target for this loop. This will allow it to be _reused_ as the continue | |
| // target for a single block loop if needed (which may have this same block as the | |
| // continue). | |
| continue_targets_.erase(continue_id); | |
| // Push id stack entry for the continuing block. We don't use EmitBlockParent to do this | |
| // because we need the scope to exist until after we process any `continue_blk_phis_`. | |
| id_stack_.emplace_back(); | |
| // We only need to emit the continuing block if: | |
| // a) It is not the loop header | |
| // b) It has inbound branches. This works around a case where you can have a continuing | |
| // where uses values which are very difficult to propagate, but the continuing is never | |
| // reached anyway, so the propagation is useless. | |
| bool skipped_continue = true; | |
| if (continue_id != src_id && !loop->Continuing()->InboundSiblingBranches().IsEmpty()) { | |
| skipped_continue = false; | |
| const auto& bb_continue = current_spirv_function_->FindBlock(continue_id); | |
| current_blocks_.insert(loop->Continuing()); | |
| TINT_CHECK_RESULT(EmitBlock(loop->Continuing(), *bb_continue)); | |
| current_blocks_.erase(loop->Continuing()); | |
| } | |
| if (!loop->Continuing()->Terminator()) { | |
| loop->Continuing()->Append(b_.NextIteration(loop)); | |
| } | |
| // If this continue block needs to pass any `phi` instructions back to the main loop body. | |
| // | |
| // We have to do this here because we need to have emitted the loop body before we can get | |
| // the values used in the continue block. | |
| auto phis = continue_blk_phis_.find(continue_id); | |
| if (phis != continue_blk_phis_.end()) { | |
| for (auto value_id : phis->second) { | |
| auto* value = skipped_continue ? nullptr : Value(value_id, false); | |
| tint::Switch( | |
| loop->Continuing()->Terminator(), // | |
| [&](core::ir::NextIteration* ni) { ni->PushOperand(value); }, | |
| [&](core::ir::BreakIf* bi) { | |
| // TODO(dsinclair): Need to change the break-if insertion if there happens | |
| // to be exit values, but those are rare, so leave this for when we have | |
| // test case. | |
| TINT_ASSERT(bi->ExitValues().empty()); | |
| auto len = bi->NextIterValues().size(); | |
| bi->PushOperand(value); | |
| bi->SetNumNextIterValues(len + 1); | |
| }, | |
| TINT_ICE_ON_NO_MATCH); | |
| } | |
| } | |
| id_stack_.pop_back(); | |
| return Success; | |
| } | |
| Result<SuccessType> ProcessInstructions(spvtools::opt::BasicBlock& src) { | |
| for (auto& inst : src) { | |
| switch (inst.opcode()) { | |
| case spv::Op::OpNop: | |
| break; | |
| case spv::Op::OpUndef: | |
| AddValue(inst.result_id(), b_.Zero(Type(inst.type_id()))); | |
| break; | |
| case spv::Op::OpBranch: | |
| TINT_CHECK_RESULT(EmitBranch(inst)); | |
| break; | |
| case spv::Op::OpBranchConditional: | |
| TINT_CHECK_RESULT(EmitBranchConditional(src, inst)); | |
| break; | |
| case spv::Op::OpSwitch: | |
| TINT_CHECK_RESULT(EmitSwitch(src, inst)); | |
| break; | |
| case spv::Op::OpLoopMerge: | |
| EmitLoopMerge(src, inst); | |
| break; | |
| case spv::Op::OpSelectionMerge: | |
| // Do nothing, the selection merge will be handled in the following | |
| // OpBranchCondition or OpSwitch instruction | |
| break; | |
| case spv::Op::OpExtInst: | |
| TINT_CHECK_RESULT(EmitExtInst(inst)); | |
| break; | |
| case spv::Op::OpCopyObject: | |
| EmitCopyObject(inst); | |
| break; | |
| case spv::Op::OpConvertFToS: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kConvertFToS); | |
| break; | |
| case spv::Op::OpConvertFToU: | |
| EmitOrAdd(b_.Convert(Type(inst.type_id()), Value(inst.GetSingleWordOperand(2))), | |
| inst.result_id()); | |
| break; | |
| case spv::Op::OpConvertSToF: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kConvertSToF); | |
| break; | |
| case spv::Op::OpConvertUToF: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kConvertUToF); | |
| break; | |
| case spv::Op::OpFConvert: | |
| EmitOrAdd(b_.Convert(Type(inst.type_id()), Value(inst.GetSingleWordOperand(2))), | |
| inst.result_id()); | |
| break; | |
| case spv::Op::OpBitwiseAnd: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kBitwiseAnd); | |
| break; | |
| case spv::Op::OpBitwiseOr: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kBitwiseOr); | |
| break; | |
| case spv::Op::OpBitwiseXor: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kBitwiseXor); | |
| break; | |
| case spv::Op::OpAccessChain: | |
| case spv::Op::OpInBoundsAccessChain: | |
| EmitAccess(inst); | |
| break; | |
| case spv::Op::OpCompositeInsert: | |
| EmitCompositeInsert(inst); | |
| break; | |
| case spv::Op::OpCompositeConstruct: | |
| EmitConstruct(inst); | |
| break; | |
| case spv::Op::OpCompositeExtract: | |
| EmitCompositeExtract(inst); | |
| break; | |
| case spv::Op::OpVectorInsertDynamic: | |
| EmitVectorInsertDynamic(inst); | |
| break; | |
| case spv::Op::OpFAdd: | |
| EmitBinary(inst, core::BinaryOp::kAdd); | |
| break; | |
| case spv::Op::OpIAdd: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kAdd); | |
| break; | |
| case spv::Op::OpSDiv: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSDiv); | |
| break; | |
| case spv::Op::OpFDiv: | |
| case spv::Op::OpUDiv: | |
| EmitBinary(inst, core::BinaryOp::kDivide); | |
| break; | |
| case spv::Op::OpIMul: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kMul); | |
| break; | |
| case spv::Op::OpFMul: | |
| case spv::Op::OpVectorTimesScalar: | |
| case spv::Op::OpMatrixTimesScalar: | |
| case spv::Op::OpVectorTimesMatrix: | |
| case spv::Op::OpMatrixTimesVector: | |
| case spv::Op::OpMatrixTimesMatrix: | |
| EmitBinary(inst, core::BinaryOp::kMultiply); | |
| break; | |
| case spv::Op::OpFRem: | |
| case spv::Op::OpUMod: | |
| EmitBinary(inst, core::BinaryOp::kModulo); | |
| break; | |
| case spv::Op::OpSMod: | |
| case spv::Op::OpSRem: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSMod); | |
| break; | |
| case spv::Op::OpFSub: | |
| EmitBinary(inst, core::BinaryOp::kSubtract); | |
| break; | |
| case spv::Op::OpFOrdEqual: | |
| EmitBinary(inst, core::BinaryOp::kEqual); | |
| break; | |
| case spv::Op::OpFOrdNotEqual: | |
| EmitBinary(inst, core::BinaryOp::kNotEqual); | |
| break; | |
| case spv::Op::OpFOrdGreaterThan: | |
| EmitBinary(inst, core::BinaryOp::kGreaterThan); | |
| break; | |
| case spv::Op::OpFOrdGreaterThanEqual: | |
| EmitBinary(inst, core::BinaryOp::kGreaterThanEqual); | |
| break; | |
| case spv::Op::OpFOrdLessThan: | |
| EmitBinary(inst, core::BinaryOp::kLessThan); | |
| break; | |
| case spv::Op::OpFOrdLessThanEqual: | |
| EmitBinary(inst, core::BinaryOp::kLessThanEqual); | |
| break; | |
| case spv::Op::OpFUnordEqual: | |
| EmitInvertedBinary(inst, core::BinaryOp::kNotEqual); | |
| break; | |
| case spv::Op::OpFUnordNotEqual: | |
| EmitInvertedBinary(inst, core::BinaryOp::kEqual); | |
| break; | |
| case spv::Op::OpFUnordGreaterThan: | |
| EmitInvertedBinary(inst, core::BinaryOp::kLessThanEqual); | |
| break; | |
| case spv::Op::OpFUnordGreaterThanEqual: | |
| EmitInvertedBinary(inst, core::BinaryOp::kLessThan); | |
| break; | |
| case spv::Op::OpFUnordLessThan: | |
| EmitInvertedBinary(inst, core::BinaryOp::kGreaterThanEqual); | |
| break; | |
| case spv::Op::OpFUnordLessThanEqual: | |
| EmitInvertedBinary(inst, core::BinaryOp::kGreaterThan); | |
| break; | |
| case spv::Op::OpIEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kEqual); | |
| break; | |
| case spv::Op::OpINotEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kNotEqual); | |
| break; | |
| case spv::Op::OpSGreaterThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSGreaterThan); | |
| break; | |
| case spv::Op::OpSGreaterThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSGreaterThanEqual); | |
| break; | |
| case spv::Op::OpSLessThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSLessThan); | |
| break; | |
| case spv::Op::OpSLessThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSLessThanEqual); | |
| break; | |
| case spv::Op::OpUGreaterThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kUGreaterThan); | |
| break; | |
| case spv::Op::OpUGreaterThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kUGreaterThanEqual); | |
| break; | |
| case spv::Op::OpULessThan: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kULessThan); | |
| break; | |
| case spv::Op::OpULessThanEqual: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kULessThanEqual); | |
| break; | |
| case spv::Op::OpISub: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSub); | |
| break; | |
| case spv::Op::OpFunctionCall: | |
| EmitFunctionCall(inst); | |
| break; | |
| case spv::Op::OpLoad: | |
| Emit(b_.Load(Value(inst.GetSingleWordOperand(2))), inst.result_id()); | |
| break; | |
| case spv::Op::OpReturn: | |
| EmitWithoutResult(b_.Return(current_function_)); | |
| break; | |
| case spv::Op::OpReturnValue: | |
| EmitWithoutResult( | |
| b_.Return(current_function_, Value(inst.GetSingleWordOperand(0)))); | |
| break; | |
| case spv::Op::OpStore: | |
| EmitWithoutResult(b_.Store(Value(inst.GetSingleWordOperand(0)), | |
| Value(inst.GetSingleWordOperand(1)))); | |
| break; | |
| case spv::Op::OpCopyMemory: | |
| EmitCopyMemory(inst); | |
| break; | |
| case spv::Op::OpVariable: | |
| TINT_CHECK_RESULT(EmitVar(inst)); | |
| break; | |
| case spv::Op::OpUnreachable: | |
| EmitWithoutResult(b_.Unreachable()); | |
| break; | |
| case spv::Op::OpKill: | |
| case spv::Op::OpTerminateInvocation: | |
| EmitKill(inst); | |
| break; | |
| case spv::Op::OpDot: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDot); | |
| break; | |
| case spv::Op::OpBitCount: | |
| EmitBitCount(inst); | |
| break; | |
| case spv::Op::OpBitFieldInsert: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kBitFieldInsert); | |
| break; | |
| case spv::Op::OpBitFieldSExtract: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kBitFieldSExtract); | |
| break; | |
| case spv::Op::OpBitFieldUExtract: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kBitFieldUExtract); | |
| break; | |
| case spv::Op::OpBitReverse: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kReverseBits); | |
| break; | |
| case spv::Op::OpAll: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kAll); | |
| break; | |
| case spv::Op::OpAny: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kAny); | |
| break; | |
| case spv::Op::OpDPdx: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDpdx); | |
| break; | |
| case spv::Op::OpDPdy: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDpdy); | |
| break; | |
| case spv::Op::OpFwidth: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kFwidth); | |
| break; | |
| case spv::Op::OpDPdxFine: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDpdxFine); | |
| break; | |
| case spv::Op::OpDPdyFine: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDpdyFine); | |
| break; | |
| case spv::Op::OpFwidthFine: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kFwidthFine); | |
| break; | |
| case spv::Op::OpDPdxCoarse: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDpdxCoarse); | |
| break; | |
| case spv::Op::OpDPdyCoarse: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kDpdyCoarse); | |
| break; | |
| case spv::Op::OpFwidthCoarse: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kFwidthCoarse); | |
| break; | |
| case spv::Op::OpLogicalAnd: | |
| EmitBinary(inst, core::BinaryOp::kAnd); | |
| break; | |
| case spv::Op::OpLogicalOr: | |
| EmitBinary(inst, core::BinaryOp::kOr); | |
| break; | |
| case spv::Op::OpLogicalEqual: | |
| EmitBinary(inst, core::BinaryOp::kEqual); | |
| break; | |
| case spv::Op::OpLogicalNotEqual: | |
| EmitBinary(inst, core::BinaryOp::kNotEqual); | |
| break; | |
| case spv::Op::OpLogicalNot: | |
| EmitUnary(inst, core::UnaryOp::kNot); | |
| break; | |
| case spv::Op::OpFNegate: | |
| EmitUnary(inst, core::UnaryOp::kNegation); | |
| break; | |
| case spv::Op::OpNot: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kNot); | |
| break; | |
| case spv::Op::OpShiftLeftLogical: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kShiftLeftLogical); | |
| break; | |
| case spv::Op::OpShiftRightLogical: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kShiftRightLogical); | |
| break; | |
| case spv::Op::OpShiftRightArithmetic: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kShiftRightArithmetic); | |
| break; | |
| case spv::Op::OpBitcast: | |
| EmitBitcast(inst); | |
| break; | |
| case spv::Op::OpQuantizeToF16: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kQuantizeToF16); | |
| break; | |
| case spv::Op::OpTranspose: | |
| EmitBuiltinCall(inst, core::BuiltinFn::kTranspose); | |
| break; | |
| case spv::Op::OpSNegate: | |
| EmitSpirvExplicitBuiltinCall(inst, spirv::BuiltinFn::kSNegate); | |
| break; | |
| case spv::Op::OpFMod: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kFMod); | |
| break; | |
| case spv::Op::OpSelect: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kSelect); | |
| break; | |
| case spv::Op::OpVectorExtractDynamic: | |
| EmitAccess(inst); | |
| break; | |
| case spv::Op::OpOuterProduct: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kOuterProduct); | |
| break; | |
| case spv::Op::OpVectorShuffle: | |
| EmitVectorShuffle(inst); | |
| break; | |
| case spv::Op::OpAtomicStore: | |
| EmitAtomicStore(inst); | |
| break; | |
| case spv::Op::OpAtomicLoad: | |
| CheckAtomicNotFloat(inst); | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicLoad); | |
| break; | |
| case spv::Op::OpAtomicIAdd: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicIAdd); | |
| break; | |
| case spv::Op::OpAtomicISub: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicISub); | |
| break; | |
| case spv::Op::OpAtomicAnd: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicAnd); | |
| break; | |
| case spv::Op::OpAtomicOr: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicOr); | |
| break; | |
| case spv::Op::OpAtomicXor: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicXor); | |
| break; | |
| case spv::Op::OpAtomicSMin: | |
| EmitAtomicSigned(inst, spirv::BuiltinFn::kAtomicSMin); | |
| break; | |
| case spv::Op::OpAtomicUMin: | |
| EmitAtomicUnsigned(inst, spirv::BuiltinFn::kAtomicUMin); | |
| break; | |
| case spv::Op::OpAtomicSMax: | |
| EmitAtomicSigned(inst, spirv::BuiltinFn::kAtomicSMax); | |
| break; | |
| case spv::Op::OpAtomicUMax: | |
| EmitAtomicUnsigned(inst, spirv::BuiltinFn::kAtomicUMax); | |
| break; | |
| case spv::Op::OpAtomicExchange: | |
| CheckAtomicNotFloat(inst); | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicExchange); | |
| break; | |
| case spv::Op::OpAtomicCompareExchange: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicCompareExchange); | |
| break; | |
| case spv::Op::OpAtomicIIncrement: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicIIncrement); | |
| break; | |
| case spv::Op::OpAtomicIDecrement: | |
| EmitSpirvBuiltinCall(inst, spirv::BuiltinFn::kAtomicIDecrement); | |
| break; | |
| case spv::Op::OpControlBarrier: | |
| TINT_CHECK_RESULT(EmitControlBarrier(inst)); | |
| break; | |
| case spv::Op::OpArrayLength: | |
| EmitArrayLength(inst); | |
| break; | |
| case spv::Op::OpSampledImage: | |
| EmitSampledImage(inst); | |
| break; | |
| case spv::Op::OpImage: | |
| EmitImage(inst); | |
| break; | |
| case spv::Op::OpImageFetch: | |
| EmitImageFetchOrRead(inst, spirv::BuiltinFn::kImageFetch); | |
| break; | |
| case spv::Op::OpImageRead: | |
| EmitImageFetchOrRead(inst, spirv::BuiltinFn::kImageRead); | |
| break; | |
| case spv::Op::OpImageGather: | |
| EmitImageGather(inst); | |
| break; | |
| case spv::Op::OpImageQueryLevels: | |
| EmitImageQuery(inst, spirv::BuiltinFn::kImageQueryLevels); | |
| break; | |
| case spv::Op::OpImageQuerySamples: | |
| EmitImageQuery(inst, spirv::BuiltinFn::kImageQuerySamples); | |
| break; | |
| case spv::Op::OpImageQuerySize: | |
| EmitImageQuery(inst, spirv::BuiltinFn::kImageQuerySize); | |
| break; | |
| case spv::Op::OpImageQuerySizeLod: | |
| EmitImageQuerySizeLod(inst); | |
| break; | |
| case spv::Op::OpImageSampleExplicitLod: | |
| EmitImageSample(inst, spirv::BuiltinFn::kImageSampleExplicitLod); | |
| break; | |
| case spv::Op::OpImageSampleImplicitLod: | |
| EmitImageSample(inst, spirv::BuiltinFn::kImageSampleImplicitLod); | |
| break; | |
| case spv::Op::OpImageSampleProjImplicitLod: | |
| EmitImageSample(inst, spirv::BuiltinFn::kImageSampleProjImplicitLod); | |
| break; | |
| case spv::Op::OpImageSampleProjExplicitLod: | |
| EmitImageSample(inst, spirv::BuiltinFn::kImageSampleProjExplicitLod); | |
| break; | |
| case spv::Op::OpImageWrite: | |
| EmitImageWrite(inst); | |
| break; | |
| case spv::Op::OpImageSampleDrefImplicitLod: | |
| EmitImageSampleDepth(inst, spirv::BuiltinFn::kImageSampleDrefImplicitLod); | |
| break; | |
| case spv::Op::OpImageSampleDrefExplicitLod: | |
| EmitImageSampleDepth(inst, spirv::BuiltinFn::kImageSampleDrefExplicitLod); | |
| break; | |
| case spv::Op::OpImageSampleProjDrefImplicitLod: | |
| EmitImageSampleDepth(inst, spirv::BuiltinFn::kImageSampleProjDrefImplicitLod); | |
| break; | |
| case spv::Op::OpImageSampleProjDrefExplicitLod: | |
| EmitImageSampleDepth(inst, spirv::BuiltinFn::kImageSampleProjDrefExplicitLod); | |
| break; | |
| case spv::Op::OpImageDrefGather: | |
| EmitImageGatherDref(inst); | |
| break; | |
| case spv::Op::OpPhi: | |
| EmitPhi(inst); | |
| break; | |
| case spv::Op::OpGroupNonUniformBroadcast: | |
| EmitSubgroupBuiltinConstantId(inst, | |
| spirv::BuiltinFn::kGroupNonUniformBroadcast); | |
| break; | |
| case spv::Op::OpGroupNonUniformQuadBroadcast: | |
| EmitSubgroupBuiltinConstantId(inst, | |
| spirv::BuiltinFn::kGroupNonUniformQuadBroadcast); | |
| break; | |
| case spv::Op::OpGroupNonUniformQuadSwap: | |
| EmitSubgroupBuiltinConstantId(inst, spirv::BuiltinFn::kGroupNonUniformQuadSwap); | |
| break; | |
| case spv::Op::OpGroupNonUniformAll: | |
| EmitSubgroupBuiltin(inst, core::BuiltinFn::kSubgroupAll); | |
| break; | |
| case spv::Op::OpGroupNonUniformAllEqual: | |
| EmitSubgroupAllEqual(inst); | |
| break; | |
| case spv::Op::OpGroupNonUniformAny: | |
| EmitSubgroupBuiltin(inst, core::BuiltinFn::kSubgroupAny); | |
| break; | |
| case spv::Op::OpGroupNonUniformElect: | |
| EmitSubgroupBuiltin(inst, core::BuiltinFn::kSubgroupElect); | |
| break; | |
| case spv::Op::OpGroupNonUniformBallot: | |
| EmitSubgroupBuiltin(inst, core::BuiltinFn::kSubgroupBallot); | |
| break; | |
| case spv::Op::OpGroupNonUniformBallotBitCount: | |
| TINT_CHECK_RESULT(EmitSubgroupBallotBitCount(inst)); | |
| break; | |
| case spv::Op::OpGroupNonUniformBroadcastFirst: | |
| EmitSubgroupBuiltin(inst, spirv::BuiltinFn::kGroupNonUniformBroadcastFirst); | |
| break; | |
| case spv::Op::OpGroupNonUniformShuffle: | |
| EmitSubgroupBuiltin(inst, spirv::BuiltinFn::kGroupNonUniformShuffle); | |
| break; | |
| case spv::Op::OpGroupNonUniformShuffleXor: | |
| EmitSubgroupBuiltin(inst, spirv::BuiltinFn::kGroupNonUniformShuffleXor); | |
| break; | |
| case spv::Op::OpGroupNonUniformShuffleDown: | |
| EmitSubgroupBuiltin(inst, spirv::BuiltinFn::kGroupNonUniformShuffleDown); | |
| break; | |
| case spv::Op::OpGroupNonUniformShuffleUp: | |
| EmitSubgroupBuiltin(inst, spirv::BuiltinFn::kGroupNonUniformShuffleUp); | |
| break; | |
| case spv::Op::OpGroupNonUniformSMin: | |
| TINT_CHECK_RESULT( | |
| EmitSubgroupMinMax(inst, spirv::BuiltinFn::kGroupNonUniformSMin)); | |
| break; | |
| case spv::Op::OpGroupNonUniformSMax: | |
| TINT_CHECK_RESULT( | |
| EmitSubgroupMinMax(inst, spirv::BuiltinFn::kGroupNonUniformSMax)); | |
| break; | |
| case spv::Op::OpGroupNonUniformUMin: | |
| case spv::Op::OpGroupNonUniformFMin: | |
| TINT_CHECK_RESULT(EmitSubgroupMinMax(inst, core::BuiltinFn::kSubgroupMin)); | |
| break; | |
| case spv::Op::OpGroupNonUniformUMax: | |
| case spv::Op::OpGroupNonUniformFMax: | |
| TINT_CHECK_RESULT(EmitSubgroupMinMax(inst, core::BuiltinFn::kSubgroupMax)); | |
| break; | |
| case spv::Op::OpGroupNonUniformIAdd: | |
| case spv::Op::OpGroupNonUniformFAdd: | |
| EmitSubgroupAdd(inst); | |
| break; | |
| case spv::Op::OpGroupNonUniformIMul: | |
| case spv::Op::OpGroupNonUniformFMul: | |
| EmitSubgroupMul(inst); | |
| break; | |
| case spv::Op::OpGroupNonUniformBitwiseAnd: | |
| TINT_CHECK_RESULT(EmitSubgroupBitwise(inst, core::BuiltinFn::kSubgroupAnd)); | |
| break; | |
| case spv::Op::OpGroupNonUniformBitwiseOr: | |
| TINT_CHECK_RESULT(EmitSubgroupBitwise(inst, core::BuiltinFn::kSubgroupOr)); | |
| break; | |
| case spv::Op::OpGroupNonUniformBitwiseXor: | |
| TINT_CHECK_RESULT(EmitSubgroupBitwise(inst, core::BuiltinFn::kSubgroupXor)); | |
| break; | |
| case spv::Op::OpIsNan: | |
| return Failure( | |
| "IsNan is not supported because NaNs cannot be represented in WGSL"); | |
| case spv::Op::OpIsInf: | |
| return Failure( | |
| "IsInf is not supported because Infinities cannot be represented in WGSL"); | |
| default: | |
| return Failure("unhandled SPIR-V instruction: " + | |
| std::string(spv::OpToString(inst.opcode())) + " (val = " + | |
| std::to_string(static_cast<uint32_t>(inst.opcode())) + ")"); | |
| } | |
| } | |
| return Success; | |
| } | |
| void AssertSubgroupScope(spvtools::opt::Instruction& inst) { | |
| auto scope_val = Value(inst.GetSingleWordInOperand(0)); | |
| auto* cnst = scope_val->As<core::ir::Constant>(); | |
| TINT_ASSERT(cnst); | |
| uint32_t scope = cnst->Value()->ValueAs<uint32_t>(); | |
| TINT_ASSERT(static_cast<spv::Scope>(scope) == spv::Scope::Subgroup) | |
| << "subgroup scope required for GroupNonUniform instructions"; | |
| } | |
| void EmitOrAdd(core::ir::Value* value, uint32_t result_id) { | |
| if (auto* inst = value->AsInstruction()) { | |
| Emit(inst, result_id); | |
| } else { | |
| AddValue(result_id, value); | |
| } | |
| } | |
| Result<SuccessType> EmitSubgroupBitwise(spvtools::opt::Instruction& inst, core::BuiltinFn fn) { | |
| AssertSubgroupScope(inst); | |
| auto group = inst.GetSingleWordInOperand(1); | |
| if (static_cast<spv::GroupOperation>(group) != spv::GroupOperation::Reduce) { | |
| return Failure("GroupNonUniformBitwise operations require a Reduce group operation"); | |
| } | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, Args(inst, 4)), inst.result_id()); | |
| return Success; | |
| } | |
| void EmitSubgroupMul(spvtools::opt::Instruction& inst) { | |
| AssertSubgroupScope(inst); | |
| core::BuiltinFn fn = core::BuiltinFn::kNone; | |
| auto group = inst.GetSingleWordInOperand(1); | |
| if (static_cast<spv::GroupOperation>(group) == spv::GroupOperation::Reduce) { | |
| fn = core::BuiltinFn::kSubgroupMul; | |
| } else if (static_cast<spv::GroupOperation>(group) == spv::GroupOperation::InclusiveScan) { | |
| fn = core::BuiltinFn::kSubgroupInclusiveMul; | |
| } else if (static_cast<spv::GroupOperation>(group) == spv::GroupOperation::ExclusiveScan) { | |
| fn = core::BuiltinFn::kSubgroupExclusiveMul; | |
| } else { | |
| TINT_ICE() << "GroupNonUniform Mul instruction must have a group of `Reduce`, " | |
| "`InclusiveScan`, or `ExclusiveScan`"; | |
| } | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, Args(inst, 4)), inst.result_id()); | |
| } | |
| void EmitSubgroupAdd(spvtools::opt::Instruction& inst) { | |
| AssertSubgroupScope(inst); | |
| core::BuiltinFn fn = core::BuiltinFn::kNone; | |
| auto group = inst.GetSingleWordInOperand(1); | |
| if (static_cast<spv::GroupOperation>(group) == spv::GroupOperation::Reduce) { | |
| fn = core::BuiltinFn::kSubgroupAdd; | |
| } else if (static_cast<spv::GroupOperation>(group) == spv::GroupOperation::InclusiveScan) { | |
| fn = core::BuiltinFn::kSubgroupInclusiveAdd; | |
| } else if (static_cast<spv::GroupOperation>(group) == spv::GroupOperation::ExclusiveScan) { | |
| fn = core::BuiltinFn::kSubgroupExclusiveAdd; | |
| } else { | |
| TINT_ICE() << "GroupNonUniform Add instruction must have a group of `Reduce`, " | |
| "`InclusiveScan`, or `ExclusiveScan`"; | |
| } | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, Args(inst, 4)), inst.result_id()); | |
| } | |
| Result<SuccessType> EmitSubgroupMinMax(spvtools::opt::Instruction& inst, core::BuiltinFn fn) { | |
| AssertSubgroupScope(inst); | |
| auto group = inst.GetSingleWordInOperand(1); | |
| if (static_cast<spv::GroupOperation>(group) != spv::GroupOperation::Reduce) { | |
| return Failure("group operand Reduce required for `Min`/`Max` instructions"); | |
| } | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, Args(inst, 4)), inst.result_id()); | |
| return Success; | |
| } | |
| Result<SuccessType> EmitSubgroupMinMax(spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| AssertSubgroupScope(inst); | |
| auto group = inst.GetSingleWordInOperand(1); | |
| if (static_cast<spv::GroupOperation>(group) != spv::GroupOperation::Reduce) { | |
| return Failure("group operand Reduce required for `Min`/`Max` instructions"); | |
| } | |
| Emit( | |
| b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, // | |
| Vector{Value(inst.GetSingleWordInOperand(0)), // | |
| b_.Constant(u32(inst.GetSingleWordInOperand(1))), | |
| Value(inst.GetSingleWordInOperand(2))}), | |
| inst.result_id()); | |
| return Success; | |
| } | |
| void EmitSubgroupBuiltinConstantId(spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| auto id = Value(inst.GetSingleWordInOperand(2)); | |
| // TODO(431054356): Convert core::BuiltinFn::kSubgroupBroadcast non-constant values into a | |
| // `subgroupShuffle` when we support SPIR-V >= 1.5 source. | |
| // | |
| // For QuadBroadcast this will remain an error as there is no WGSL equivalent. | |
| // For QuadSwap this will remain an error as there is no WGSL equivalent. | |
| TINT_ASSERT(id->Is<core::ir::Constant>()) | |
| << "non-constant GroupNonUniform `Invocation Id` not supported"; | |
| AssertSubgroupScope(inst); | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, Args(inst, 2)), | |
| inst.result_id()); | |
| } | |
| void EmitSubgroupBuiltin(spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| AssertSubgroupScope(inst); | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, Args(inst, 2)), | |
| inst.result_id()); | |
| } | |
| void EmitSubgroupBuiltin(spvtools::opt::Instruction& inst, core::BuiltinFn fn) { | |
| AssertSubgroupScope(inst); | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, Args(inst, 3)), inst.result_id()); | |
| } | |
| Result<SuccessType> EmitSubgroupBallotBitCount(spvtools::opt::Instruction& inst) { | |
| AssertSubgroupScope(inst); | |
| auto ballot_id = inst.GetSingleWordInOperand(2); | |
| auto* ballot_inst = spirv_context_->get_def_use_mgr()->GetDef(ballot_id); | |
| if (!ballot_inst || ballot_inst->opcode() != spv::Op::OpGroupNonUniformBallot) { | |
| return Failure( | |
| "OpGroupNonUniformBallotBitCount is only supported when Value is from " | |
| "OpGroupNonUniformBallot"); | |
| } | |
| auto group = inst.GetSingleWordInOperand(1); | |
| auto group_op = static_cast<spv::GroupOperation>(group); | |
| core::BuiltinFn fn = core::BuiltinFn::kNone; | |
| switch (group_op) { | |
| case spv::GroupOperation::Reduce: | |
| fn = core::BuiltinFn::kSubgroupAdd; | |
| break; | |
| case spv::GroupOperation::InclusiveScan: | |
| fn = core::BuiltinFn::kSubgroupInclusiveAdd; | |
| break; | |
| case spv::GroupOperation::ExclusiveScan: | |
| fn = core::BuiltinFn::kSubgroupExclusiveAdd; | |
| break; | |
| default: | |
| TINT_UNREACHABLE(); | |
| } | |
| auto* pred_val = Value(ballot_inst->GetSingleWordInOperand(1)); | |
| auto* conv = b_.Convert<u32>(pred_val); | |
| EmitWithoutSpvResult(conv); | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, conv), inst.result_id()); | |
| return Success; | |
| } | |
| void EmitSubgroupAllEqual(spvtools::opt::Instruction& inst) { | |
| AssertSubgroupScope(inst); | |
| auto* val = Value(inst.GetSingleWordInOperand(1)); | |
| auto* first = b_.Call<spirv::ir::BuiltinCall>( | |
| val->Type(), spirv::BuiltinFn::kGroupNonUniformBroadcastFirst, | |
| Vector{Value(inst.GetSingleWordInOperand(0)), val}); | |
| EmitWithoutSpvResult(first->Result()); | |
| // If NaNs and INFs are supported in the future, this lowering will remain correct as long | |
| // as floating-point equality comparisons use ordered-and-equal (OpFOrdEqual) semantics. | |
| auto* eq = b_.Equal(val, first); | |
| EmitWithoutSpvResult(eq); | |
| core::ir::Value* eq_val = eq; | |
| if (val->Type()->Is<core::type::Vector>()) { | |
| auto* all_call = b_.Call(ty_.bool_(), core::BuiltinFn::kAll, eq); | |
| EmitWithoutSpvResult(all_call); | |
| eq_val = all_call; | |
| } | |
| EmitOrAdd(b_.Call(ty_.bool_(), core::BuiltinFn::kSubgroupAll, eq_val), inst.result_id()); | |
| } | |
| struct IfBranchValue { | |
| core::ir::Value* value; | |
| core::ir::If* if_; | |
| }; | |
| void EmitPhi(spvtools::opt::Instruction& inst) { | |
| auto num_ops = inst.NumInOperands(); | |
| // If there are no operands, then the phi is in a unreachable block, ignore it. | |
| if (num_ops == 0) { | |
| return; | |
| } | |
| // If there are only 2 arguments, that means we came directly from a block, so just emit the | |
| // value directly. | |
| if (num_ops == 2) { | |
| AddValue(inst.result_id(), Value(inst.GetSingleWordInOperand(0))); | |
| return; | |
| } | |
| auto* phi_containing_spirv_block = spirv_context_->get_instr_block(&inst); | |
| auto iter = spirv_merge_id_to_header_id_.find(phi_containing_spirv_block->id()); | |
| // We're in a merge block for some construct which could be a loop, switch or if. | |
| if (iter != spirv_merge_id_to_header_id_.end()) { | |
| auto parent_blk_id = iter->second; | |
| auto* parent_blk = spirv_context_->get_instr_block(parent_blk_id); | |
| if (parent_blk->terminator()->opcode() == spv::Op::OpSwitch) { | |
| EmitPhiInSwitchMerge(inst, parent_blk_id); | |
| return; | |
| } | |
| if (!parent_blk->IsLoopHeader()) { | |
| EmitPhiInIfMerge(inst, parent_blk_id); | |
| return; | |
| } | |
| } | |
| // At this point, we must be dealing with an OpPhi in some kind of loop context, the loop | |
| // header, loop continuing or loop merge construct. | |
| EmitPhiInLoop(inst); | |
| } | |
| void EmitPhiInLoop(spvtools::opt::Instruction& inst) { | |
| auto* phi_containing_spirv_block = spirv_context_->get_instr_block(&inst); | |
| auto iter = spirv_merge_id_to_header_id_.find(phi_containing_spirv_block->id()); | |
| // We're in the merge block for the loop | |
| if (iter != spirv_merge_id_to_header_id_.end()) { | |
| EmitPhiInLoopMerge(inst); | |
| return; | |
| } | |
| // The OpPhi is in the loop header, which means it's the body of the loop. The OpPhi can | |
| // receive values from the parent block, in which case we need to push them through the | |
| // initializer, or from the continuing block, which we will not have emitted yet. | |
| if (phi_containing_spirv_block->IsLoopHeader()) { | |
| EmitPhiInLoopHeader(inst, phi_containing_spirv_block); | |
| return; | |
| } | |
| auto containing_loop_header_id = spirv_context_->GetStructuredCFGAnalysis()->ContainingLoop( | |
| phi_containing_spirv_block->id()); | |
| auto* loop_header_block = spirv_context_->get_instr_block(containing_loop_header_id); | |
| TINT_ASSERT(loop_header_block); | |
| auto* loop_merge_inst = loop_header_block->GetLoopMergeInst(); | |
| auto continue_id = loop_merge_inst->GetSingleWordInOperand(1); | |
| // If the spirv-id to block table contains an entry for the continue id then we must have | |
| // started emitting the continue. | |
| bool has_continue_emitted = spirv_id_to_block_.contains(continue_id); | |
| if (!has_continue_emitted) { | |
| EmitPhiInLoopBody(inst, loop_merge_inst); | |
| return; | |
| } | |
| // The OpPhi is in the continue block of the loop. | |
| EmitPhiInLoopContinue(inst); | |
| } | |
| void AddOperandToTerminator(core::ir::Terminator* term, uint32_t id) { | |
| // If the ID is a constant, then we just directly emit it, it isn't an OpPhi value | |
| if (IdIsConstant(id)) { | |
| term->PushOperand(Value(id, false)); | |
| return; | |
| } | |
| // If we've already seen the value, then we can emit it as this isn't referencing a later | |
| // value. | |
| auto val = values_.Get(id); | |
| if (val) { | |
| // If we've already seen the value, and it's still in scope, then we can just emit as it | |
| // isn't referencing a later value. | |
| if (IdIsInScope(id) || (*val)->Is<core::ir::FunctionParam>()) { | |
| term->PushOperand(Value(id, false)); | |
| return; | |
| } | |
| // The value was a block param, that means it was a Phi which we've removed and | |
| // converted. We need to pass that Phi out of the control instruction, but we only want | |
| // to add it to exit instructions, not all terminators, so this isn't a full | |
| // Propagation. | |
| if (auto* bp = (*val)->As<core::ir::BlockParam>()) { | |
| auto* v = PropagateTerm(term, id, bp, bp->Block()); | |
| term->PushOperand(v); | |
| return; | |
| } | |
| // The value is defined by an instruction inside an inner block. Propagate it out of | |
| // that block, then push that propagated value onto this terminator. | |
| if (auto* res = (*val)->As<core::ir::InstructionResult>()) { | |
| auto* v = PropagateTerm(term, id, *val, res->Instruction()->Block()); | |
| term->PushOperand(v); | |
| return; | |
| } | |
| } | |
| // Value isn't known, or isn't in scope, push a placeholder for the operand value. We store | |
| // away the terminator/index pair along with the required value and then fill it in at the | |
| // end of the block emission. | |
| auto operand_idx = term->PushOperand(nullptr); | |
| values_to_replace_.back().push_back(ReplacementValue{ | |
| .terminator = term, | |
| .idx = operand_idx, | |
| .value_id = id, | |
| }); | |
| } | |
| void EmitPhiInLoopMerge(spvtools::opt::Instruction& inst) { | |
| // All of the source blocks should exist as they will either be the loop header, if we | |
| // happened to jump out early, the body if we break or the continuing if we break-if. | |
| auto* type = Type(inst.type_id()); | |
| auto* phi_spirv_block = spirv_context_->get_instr_block(&inst); | |
| auto phi_blk_id = phi_spirv_block->id(); | |
| // The merge target (which is the OpPhi SPIR-V block) is a walk stop block | |
| auto* loop = StopWalkingAt(phi_spirv_block->id())->As<core::ir::Loop>(); | |
| TINT_ASSERT(loop); | |
| std::optional<uint32_t> default_value = std::nullopt; | |
| // The only way into the continue should be through `continue` calls out of the loop body, | |
| // which should all exist at this point as we emit the body first. | |
| for (uint32_t i = 0; i < inst.NumInOperands(); i += 2) { | |
| auto value_id = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| core::ir::Terminator* term = nullptr; | |
| // If the basic block ends in a branch conditional, then we really need to update the | |
| // branch which goes to the current block, not the terminator of the block we emitted | |
| // into. This difference is because the merge block will _also_ end up in the same IR | |
| // block and we can't tell the difference if we just get the terminator. | |
| const auto& bb = current_spirv_function_->FindBlock(blk_id); | |
| auto* terminator = (*bb).terminator(); | |
| if (terminator->opcode() == spv::Op::OpBranchConditional) { | |
| uint32_t true_id = terminator->GetSingleWordInOperand(1); | |
| uint32_t false_id = terminator->GetSingleWordInOperand(2); | |
| auto iter = branch_conditional_to_if_.find(terminator); | |
| if (iter != branch_conditional_to_if_.end()) { | |
| auto* if_ = iter->second; | |
| if (true_id == phi_blk_id) { | |
| term = if_->True()->Terminator(); | |
| } else if (false_id == phi_blk_id) { | |
| term = if_->False()->Terminator(); | |
| } | |
| } | |
| } | |
| if (term == nullptr) { | |
| auto value_blk_iter = spirv_id_to_block_.find(blk_id); | |
| auto* value_ir_blk = value_blk_iter->second; | |
| term = value_ir_blk->Terminator(); | |
| } | |
| if (term->Is<core::ir::Unreachable>()) { | |
| default_value = value_id; | |
| continue; | |
| } | |
| // Continue isn't a terminator into a merge. So, we must have had a branch conditional | |
| // where one branch continued and the other exited. We need to update the exiting block | |
| // as the terminator. | |
| if (term->Is<core::ir::Continue>()) { | |
| auto* if_ = term->prev->As<core::ir::If>(); | |
| TINT_ASSERT(if_); | |
| if (if_->True()->Terminator()->Is<core::ir::ExitLoop>()) { | |
| term = if_->True()->Terminator(); | |
| } else { | |
| term = if_->False()->Terminator(); | |
| TINT_ASSERT(term->Is<core::ir::ExitLoop>()); | |
| } | |
| } | |
| AddOperandToTerminator(term, value_id); | |
| } | |
| auto* res = b_.InstructionResult(type); | |
| loop->AddResult(res); | |
| AddValue(inst.result_id(), res); | |
| // If there is a default value, then one of the blocks was an unreachable at the end. We | |
| // need to find exits where we haven't set all the params and add the default. | |
| if (default_value) { | |
| auto result_count = loop->Results().Length(); | |
| for (auto iter : loop->Exits()) { | |
| if (iter->Operands().Length() >= result_count) { | |
| continue; | |
| } | |
| // We haven't updated the exit yet | |
| auto* term = iter->As<core::ir::Terminator>(); | |
| TINT_ASSERT(term); | |
| AddOperandToTerminator(term, default_value.value()); | |
| } | |
| } | |
| } | |
| void EmitPhiInLoopContinue(spvtools::opt::Instruction& inst) { | |
| auto* type = Type(inst.type_id()); | |
| auto* phi_spirv_block = spirv_context_->get_instr_block(&inst); | |
| // The continue target is a walk stop block | |
| auto* loop = StopWalkingAt(phi_spirv_block->id())->As<core::ir::Loop>(); | |
| TINT_ASSERT(loop); | |
| std::optional<uint32_t> default_value = std::nullopt; | |
| // The only way into the continue should be through `continue` calls out of the loop body, | |
| // which should all exist at this point as we emit the body first. | |
| for (uint32_t i = 0; i < inst.NumInOperands(); i += 2) { | |
| auto value_id = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| auto value_blk_iter = spirv_id_to_block_.find(blk_id); | |
| auto* value_ir_blk = value_blk_iter->second; | |
| auto* term = value_ir_blk->Terminator(); | |
| if (term->Is<core::ir::Unreachable>()) { | |
| default_value = value_id; | |
| continue; | |
| } | |
| AddOperandToTerminator(term, value_id); | |
| } | |
| // Add the block param to the continuing block | |
| auto* p = b_.BlockParam(type); | |
| loop->Continuing()->AddParam(p); | |
| AddValue(inst.result_id(), p); | |
| // If there is a default value, then one of the blocks was an unreachable at the end, this | |
| // can happen if the `OpBranchConditional` has one of the branches to the continue block. We | |
| // need to find incoming sibling branches where we haven't set all the params and add the | |
| // default. | |
| if (default_value) { | |
| auto param_count = loop->Continuing()->Params().Length(); | |
| for (auto* sibling : loop->Continuing()->InboundSiblingBranches()) { | |
| if (sibling->Operands().Length() >= param_count) { | |
| continue; | |
| } | |
| // We haven't updated the sibling yet | |
| auto* term = sibling->As<core::ir::Terminator>(); | |
| TINT_ASSERT(term); | |
| AddOperandToTerminator(term, default_value.value()); | |
| } | |
| } | |
| } | |
| void EmitPhiInLoopBody(spvtools::opt::Instruction& inst, | |
| spvtools::opt::Instruction* loop_merge_inst) { | |
| auto continue_id = loop_merge_inst->GetSingleWordInOperand(1); | |
| auto* loop_header_block = spirv_context_->get_instr_block(loop_merge_inst); | |
| // The loop continue is a walk stop block. Since we're in the loop body itself we would have | |
| // added the continue target to the stop blocks when processing the header loop merge. | |
| auto* loop = StopWalkingAt(continue_id)->As<core::ir::Loop>(); | |
| TINT_ASSERT(loop); | |
| // The only phi incoming block that makes sense on the body is from the loop header itself. | |
| // Any other incoming branches must be from unreachable blocks, otherwise it would have had | |
| // to have come from the loop header which is where the continue target will loop back too, | |
| // not the body label directly. | |
| std::optional<core::ir::Value*> value = std::nullopt; | |
| for (uint32_t i = 0; i < inst.NumInOperands(); i += 2) { | |
| auto value_id = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| if (blk_id != loop_header_block->id()) { | |
| continue; | |
| } | |
| value = Value(value_id); | |
| } | |
| TINT_ASSERT(value.has_value()); | |
| AddValue(inst.result_id(), value.value()); | |
| } | |
| void EmitPhiInLoopHeader(spvtools::opt::Instruction& inst, | |
| spvtools::opt::BasicBlock* loop_header) { | |
| // Incoming branches should be from the parent block into the loop, and from the loop | |
| // continuing back into the header, each with a block_id and a value_id. | |
| TINT_ASSERT(inst.NumInOperands() == 4); | |
| auto* type = Type(inst.type_id()); | |
| auto* phi_spirv_block = spirv_context_->get_instr_block(&inst); | |
| auto* loop_merge_inst = loop_header->GetLoopMergeInst(); | |
| auto continue_id = loop_merge_inst->GetSingleWordInOperand(1); | |
| // The loop header is a walk stop block. | |
| auto* loop = StopWalkingAt(phi_spirv_block->id())->As<core::ir::Loop>(); | |
| TINT_ASSERT(loop); | |
| for (uint32_t i = 0; i < inst.NumInOperands(); i += 2) { | |
| auto value_id = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| if (block_phi_values_.contains(blk_id)) { | |
| block_phi_values_[blk_id].push_back(value_id); | |
| } else { | |
| block_phi_values_[blk_id] = {value_id}; | |
| } | |
| auto v = values_.Get(value_id); | |
| if (v) { | |
| // If the value exists, then try to get it to force the propagation. If it doesn't | |
| // exist, then it will come later in the header and we'll deal with it later. | |
| Value(value_id); | |
| } | |
| core::ir::Terminator* term = nullptr; | |
| // The referenced block hasn't been emitted yet (continue blocks have this | |
| // behaviour). So, store the fact that it needs to return a given value away for | |
| // when we do emit the block. | |
| // | |
| // We check both if we've seen the continue block, which if it's a separate block we | |
| // won't have seen, or if we have seen it (then it's the header itself) if the block is | |
| // the continue id. | |
| if (!spirv_id_to_block_.contains(blk_id) || blk_id == continue_id) { | |
| // If the continue is a separate target, we'll emit it later so just store this | |
| // value away to add to the next iteration. | |
| if (continue_id != phi_spirv_block->id()) { | |
| continue_blk_phis_[continue_id].push_back(value_id); | |
| continue; | |
| } | |
| // The loop header is the terminator, so synthesize a continue block and append | |
| // to that a next iteration. | |
| if (loop->Continuing()->IsEmpty()) { | |
| b_.Append(loop->Continuing(), [&] { term = b_.NextIteration(loop); }); | |
| } else { | |
| // With multiple phis we may have already created the continuing | |
| // block, so just get the terminator. | |
| term = loop->Continuing()->Terminator(); | |
| TINT_ASSERT(term->Is<core::ir::NextIteration>()); | |
| } | |
| } else { | |
| // This is the parent block, push this into the next iteration of the initializer. | |
| if (loop->Initializer()->IsEmpty()) { | |
| b_.Append(loop->Initializer(), [&] { term = b_.NextIteration(loop); }); | |
| } else { | |
| term = loop->Initializer()->Terminator(); | |
| TINT_ASSERT(term->Is<core::ir::NextIteration>()); | |
| } | |
| } | |
| AddOperandToTerminator(term, value_id); | |
| } | |
| // Add the block param to the body | |
| auto* p = b_.BlockParam(type); | |
| loop->Body()->AddParam(p); | |
| AddValue(inst.result_id(), p); | |
| } | |
| // Emit an OpPhi which is inside the merge block for a if. | |
| void EmitPhiInIfMerge(spvtools::opt::Instruction& inst, uint32_t header_id) { | |
| auto* type = Type(inst.type_id()); | |
| core::ir::If* ctrl = nullptr; | |
| std::optional<core::ir::Value*> value_for_default_block = std::nullopt; | |
| for (uint32_t i = 0; i < inst.NumInOperands(); i += 2) { | |
| auto value_id = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| auto value_blk_iter = spirv_id_to_block_.find(blk_id); | |
| auto* value_ir_blk = value_blk_iter->second; | |
| // The block refers to the header itself, so it was the true or false branching directly | |
| // to the merge. | |
| // | |
| // We store this away as we may not know the if yet, we need to wait until we get the | |
| // control instruction and do the work later. | |
| if (blk_id == header_id) { | |
| value_for_default_block = Value(value_id); | |
| continue; | |
| } | |
| auto* term = value_ir_blk->Terminator(); | |
| AddOperandToTerminator(term, value_id); | |
| if (auto* exit = term->As<core::ir::ExitIf>()) { | |
| if (ctrl) { | |
| TINT_ASSERT(ctrl == exit->ControlInstruction()); | |
| } else { | |
| ctrl = exit->ControlInstruction()->As<core::ir::If>(); | |
| TINT_ASSERT(ctrl); | |
| } | |
| } else { | |
| TINT_UNREACHABLE(); | |
| } | |
| } | |
| // No control instruction means that both true/false branches are the merge itself, so we | |
| // can just ignore the if, if we have a default then assign it straight through. | |
| if (!ctrl) { | |
| if (value_for_default_block) { | |
| AddValue(inst.result_id(), value_for_default_block.value()); | |
| } | |
| return; | |
| } | |
| // If the default block needs to be assigned a value | |
| if (value_for_default_block) { | |
| core::ir::Terminator* term = nullptr; | |
| if (ctrl->True()->Terminator()->Operands().Length() < | |
| ctrl->False()->Terminator()->Operands().Length()) { | |
| term = ctrl->True()->Terminator(); | |
| } else { | |
| term = ctrl->False()->Terminator(); | |
| } | |
| term->PushOperand(value_for_default_block.value()); | |
| } | |
| // Push the result into the parent | |
| auto* res = b_.InstructionResult(type); | |
| ctrl->AddResult(res); | |
| AddValue(inst.result_id(), res); | |
| } | |
| // Emit an OpPhi which is inside the merge block for a switch. | |
| void EmitPhiInSwitchMerge(spvtools::opt::Instruction& inst, uint32_t header_id) { | |
| auto* type = Type(inst.type_id()); | |
| core::ir::Switch* ctrl = nullptr; | |
| std::optional<core::ir::Value*> value_for_default_block = std::nullopt; | |
| auto phi_blk_id = spirv_context_->get_instr_block(&inst)->id(); | |
| for (uint32_t i = 0; i < inst.NumInOperands(); i += 2) { | |
| auto value_id = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| core::ir::Terminator* term = nullptr; | |
| // If the basic block ends in a branch conditional, then we really need to update the | |
| // branch which goes to the current block, not the terminator of the block we emitted | |
| // into. This difference is because the merge block will _also_ end up in the same IR | |
| // block and we can't tell the difference if we just get the terminator. | |
| const auto& bb = current_spirv_function_->FindBlock(blk_id); | |
| auto* terminator = (*bb).terminator(); | |
| if (terminator->opcode() == spv::Op::OpBranchConditional) { | |
| uint32_t true_id = terminator->GetSingleWordInOperand(1); | |
| uint32_t false_id = terminator->GetSingleWordInOperand(2); | |
| auto iter = branch_conditional_to_if_.find(terminator); | |
| TINT_ASSERT(iter != branch_conditional_to_if_.end()); | |
| auto* if_ = iter->second; | |
| if (true_id == phi_blk_id) { | |
| term = if_->True()->Terminator(); | |
| } else if (false_id == phi_blk_id) { | |
| term = if_->False()->Terminator(); | |
| } | |
| } | |
| if (term == nullptr) { | |
| auto value_blk_iter = spirv_id_to_block_.find(blk_id); | |
| auto* value_ir_blk = value_blk_iter->second; | |
| // In the case of a switch, the block can refer to the header of the switch, in this | |
| // case it means we don't have a default block and we jump over the switch itself, | |
| // so we need to insert this value into the terminator of the default block of the | |
| // switch. | |
| // | |
| // We store this away as we may not know the switch yet, we need to wait until we | |
| // get the control instruction and do the work later. | |
| if (blk_id == header_id) { | |
| value_for_default_block = Value(value_id); | |
| continue; | |
| } | |
| term = value_ir_blk->Terminator(); | |
| } | |
| AddOperandToTerminator(term, value_id); | |
| if (auto* exit = term->As<core::ir::ExitSwitch>()) { | |
| if (ctrl) { | |
| TINT_ASSERT(ctrl == exit->ControlInstruction()); | |
| } else { | |
| ctrl = exit->ControlInstruction()->As<core::ir::Switch>(); | |
| TINT_ASSERT(ctrl); | |
| } | |
| } else { | |
| TINT_UNREACHABLE(); | |
| } | |
| } | |
| // No control instruction means there were no cases, so we can just ignore the switch, if we | |
| // have a default then assign it straight through. | |
| if (!ctrl) { | |
| if (value_for_default_block) { | |
| AddValue(inst.result_id(), value_for_default_block.value()); | |
| } | |
| return; | |
| } | |
| // If the default block needs to be assigned a value | |
| if (value_for_default_block) { | |
| ctrl->DefaultBlock()->Terminator()->PushOperand(value_for_default_block.value()); | |
| } | |
| // Push the result into the switch | |
| auto* res = b_.InstructionResult(type); | |
| ctrl->AddResult(res); | |
| AddValue(inst.result_id(), res); | |
| } | |
| void EmitImage(const spvtools::opt::Instruction& inst) { | |
| auto* si = Value(inst.GetSingleWordInOperand(0)); | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>( | |
| Type(inst.type_id()), spirv::BuiltinFn::kOpImage, | |
| Vector<core::ir::TemplateParameter, 1>{ | |
| si->Type()->As<spirv::type::SampledImage>()->Image()}, | |
| Args(inst, 2)), | |
| inst.result_id()); | |
| } | |
| void EmitSampledImage(const spvtools::opt::Instruction& inst) { | |
| auto* tex = Value(inst.GetSingleWordInOperand(0)); | |
| auto* img_type = tex->Type()->As<type::Image>(); | |
| TINT_ASSERT(img_type); | |
| TINT_ASSERT(img_type->GetMultisampled() != type::Multisampled::kMultisampled) | |
| << "Creating an OpTypeSampledImage from a multisampled image is not supported"; | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>( | |
| Type(inst.type_id()), spirv::BuiltinFn::kOpSampledImage, | |
| Vector<core::ir::TemplateParameter, 1>{tex->Type()}, Args(inst, 2)), | |
| inst.result_id()); | |
| } | |
| void EmitImageFetchOrRead(const spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| auto sampled_image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* coord = Value(inst.GetSingleWordInOperand(1)); | |
| Vector<core::ir::Value*, 4> args = {sampled_image, coord}; | |
| uint32_t literal_mask = inst.NumInOperands() > 2 ? inst.GetSingleWordInOperand(2) : 0u; | |
| args.Push(Literal(literal_mask)); | |
| if (literal_mask != 0) { | |
| args.Push(Value(inst.GetSingleWordInOperand(3))); | |
| } | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, args), inst.result_id()); | |
| } | |
| void EmitImageGatherDref(const spvtools::opt::Instruction& inst) { | |
| auto sampled_image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* coord = Value(inst.GetSingleWordInOperand(1)); | |
| auto* dref = Value(inst.GetSingleWordInOperand(2)); | |
| Vector<core::ir::Value*, 4> args = {sampled_image, coord, dref}; | |
| uint32_t literal_mask = inst.NumInOperands() > 3 ? inst.GetSingleWordInOperand(3) : 0u; | |
| args.Push(Literal(literal_mask)); | |
| if (literal_mask != 0) { | |
| TINT_ASSERT(static_cast<spv::ImageOperandsMask>(literal_mask) == | |
| spv::ImageOperandsMask::ConstOffset); | |
| TINT_ASSERT(inst.NumInOperands() > 4); | |
| args.Push(Value(inst.GetSingleWordInOperand(4))); | |
| } | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), | |
| spirv::BuiltinFn::kImageDrefGather, args), | |
| inst.result_id()); | |
| } | |
| void EmitImageGather(const spvtools::opt::Instruction& inst) { | |
| auto sampled_image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* coord = Value(inst.GetSingleWordInOperand(1)); | |
| auto* comp = Value(inst.GetSingleWordInOperand(2)); | |
| Vector<core::ir::Value*, 4> args = {sampled_image, coord, comp}; | |
| uint32_t literal_mask = inst.NumInOperands() > 3 ? inst.GetSingleWordInOperand(3) : 0u; | |
| args.Push(Literal(literal_mask)); | |
| if (literal_mask != 0) { | |
| TINT_ASSERT(static_cast<spv::ImageOperandsMask>(literal_mask) == | |
| spv::ImageOperandsMask::ConstOffset); | |
| TINT_ASSERT(inst.NumInOperands() > 4); | |
| args.Push(Value(inst.GetSingleWordInOperand(4))); | |
| } | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), spirv::BuiltinFn::kImageGather, | |
| args), | |
| inst.result_id()); | |
| } | |
| void EmitImageSample(const spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| auto sampled_image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* coord = Value(inst.GetSingleWordInOperand(1)); | |
| Vector<core::ir::Value*, 4> args = {sampled_image, coord}; | |
| uint32_t literal_mask = inst.NumInOperands() > 2 ? inst.GetSingleWordInOperand(2) : 0u; | |
| args.Push(Literal(literal_mask)); | |
| TINT_ASSERT(literal_mask == 0 || inst.NumInOperands() > 3); | |
| for (uint32_t i = 3; i < inst.NumInOperands(); ++i) { | |
| args.Push(Value(inst.GetSingleWordInOperand(i))); | |
| } | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, args), inst.result_id()); | |
| } | |
| bool HasLod(uint32_t mask) { | |
| return (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Lod)) != 0; | |
| } | |
| void EmitImageSampleDepth(const spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| auto sampled_image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* coord = Value(inst.GetSingleWordInOperand(1)); | |
| auto* dref = Value(inst.GetSingleWordInOperand(2)); | |
| Vector<core::ir::Value*, 4> args = {sampled_image, coord, dref}; | |
| uint32_t literal_mask = inst.NumInOperands() > 3 ? inst.GetSingleWordInOperand(3) : 0u; | |
| args.Push(Literal(literal_mask)); | |
| TINT_ASSERT(literal_mask == 0 || inst.NumInOperands() > 4); | |
| for (uint32_t i = 4; i < inst.NumInOperands(); ++i) { | |
| args.Push(Value(inst.GetSingleWordInOperand(i))); | |
| } | |
| if (HasLod(literal_mask)) { | |
| core::ir::Value* lod = args[4]; | |
| TINT_ASSERT(lod->Is<core::ir::Constant>()); | |
| TINT_ASSERT(lod->Type()->As<core::type::F32>()); | |
| auto v = lod->As<core::ir::Constant>()->Value()->ValueAs<float>(); | |
| TINT_ASSERT(v == 0.0f) << "Dref LOD values must be 0.0"; | |
| } | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, args), inst.result_id()); | |
| } | |
| void EmitImageWrite(const spvtools::opt::Instruction& inst) { | |
| auto* image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* coord = Value(inst.GetSingleWordInOperand(1)); | |
| core::ir::Value* texel = Value(inst.GetSingleWordInOperand(2)); | |
| // Our intrinsic has a vec4 type, which matches what WGSL expects. Instead of creating more | |
| // intrinsic entries, just turn the texel into a vec4. | |
| auto* texel_ty = texel->Type(); | |
| if (texel_ty->IsScalar()) { | |
| auto* c = b_.Construct(ty_.vec4(texel_ty), texel); | |
| EmitWithoutSpvResult(c); | |
| texel = c; | |
| } else { | |
| auto* vec_ty = texel_ty->As<core::type::Vector>(); | |
| TINT_ASSERT(vec_ty); | |
| core::ir::Value* c = nullptr; | |
| if (vec_ty->Width() == 2) { | |
| c = b_.Construct(ty_.vec4(vec_ty->Type()), texel, b_.Zero(vec_ty)); | |
| } else if (vec_ty->Width() == 3) { | |
| c = b_.Construct(ty_.vec4(vec_ty->Type()), texel, b_.Zero(vec_ty->Type())); | |
| } | |
| if (c != nullptr) { | |
| EmitWithoutSpvResult(c); | |
| texel = c; | |
| } | |
| } | |
| uint32_t literal_mask = inst.NumInOperands() > 3 ? inst.GetSingleWordInOperand(3) : 0u; | |
| TINT_ASSERT(literal_mask == 0); | |
| Vector<core::ir::Value*, 4> args = {image, coord, texel, Literal(literal_mask)}; | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(ty_.void_(), spirv::BuiltinFn::kImageWrite, args), | |
| inst.result_id()); | |
| } | |
| void EmitImageQuery(const spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| auto* image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* ty = Type(inst.type_id()); | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>( | |
| ty, fn, Vector<core::ir::TemplateParameter, 1>{ty->DeepestElement()}, image), | |
| inst.result_id()); | |
| } | |
| void EmitImageQuerySizeLod(const spvtools::opt::Instruction& inst) { | |
| auto* image = Value(inst.GetSingleWordInOperand(0)); | |
| auto* level = Value(inst.GetSingleWordInOperand(1)); | |
| auto* ty = Type(inst.type_id()); | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>( | |
| ty, spirv::BuiltinFn::kImageQuerySizeLod, | |
| Vector<core::ir::TemplateParameter, 1>{ty->DeepestElement()}, image, level), | |
| inst.result_id()); | |
| } | |
| void EmitAtomicSigned(const spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| core::ir::Value* v = Value(inst.GetSingleWordOperand(2)); | |
| TINT_ASSERT(v->Type()->UnwrapPtr()->Is<core::type::I32>()); | |
| EmitSpirvBuiltinCall(inst, fn); | |
| } | |
| void EmitAtomicUnsigned(const spvtools::opt::Instruction& inst, spirv::BuiltinFn fn) { | |
| core::ir::Value* v = Value(inst.GetSingleWordOperand(2)); | |
| TINT_ASSERT(v->Type()->UnwrapPtr()->Is<core::type::U32>()); | |
| EmitSpirvBuiltinCall(inst, fn); | |
| } | |
| void EmitArrayLength(const spvtools::opt::Instruction& inst) { | |
| auto strct = Value(inst.GetSingleWordInOperand(0)); | |
| auto field_index = inst.GetSingleWordInOperand(1); | |
| auto* ptr = strct->Type()->As<core::type::Pointer>(); | |
| TINT_ASSERT(ptr); | |
| auto* ty = ptr->StoreType()->As<core::type::Struct>(); | |
| TINT_ASSERT(ty); | |
| auto* access = | |
| b_.Access(ty_.ptr(ptr->AddressSpace(), ty->Members().Back()->Type(), ptr->Access()), | |
| strct, u32(field_index)); | |
| EmitWithoutSpvResult(access); | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), core::BuiltinFn::kArrayLength, Vector{access}), | |
| inst.result_id()); | |
| } | |
| Result<SuccessType> EmitControlBarrier(const spvtools::opt::Instruction& inst) { | |
| auto get_constant = [&](uint32_t idx) { | |
| uint32_t id = inst.GetSingleWordOperand(idx); | |
| auto* constant = spirv_context_->get_constant_mgr()->FindDeclaredConstant(id); | |
| TINT_ASSERT(constant); | |
| return constant->GetU32(); | |
| }; | |
| uint32_t execution = get_constant(0); | |
| uint32_t memory = get_constant(1); | |
| uint32_t semantics = get_constant(2); | |
| if (execution != dawn::to_underlying(spv::Scope::Workgroup)) { | |
| return Failure( | |
| "unsupported control barrier execution scope: expected Workgroup, got: " + | |
| std::string(spv::ScopeToString(spv::Scope(execution)))); | |
| } | |
| if (semantics & dawn::to_underlying(spv::MemorySemanticsMask::AcquireRelease)) { | |
| semantics &= ~dawn::to_underlying(spv::MemorySemanticsMask::AcquireRelease); | |
| } else { | |
| return Failure("control barrier semantics requires acquire and release"); | |
| } | |
| if (memory != dawn::to_underlying(spv::Scope::Workgroup)) { | |
| return Failure("control barrier requires workgroup memory scope"); | |
| } | |
| if (semantics & dawn::to_underlying(spv::MemorySemanticsMask::WorkgroupMemory)) { | |
| EmitWithoutSpvResult(b_.Call(ty_.void_(), core::BuiltinFn::kWorkgroupBarrier)); | |
| semantics &= ~dawn::to_underlying(spv::MemorySemanticsMask::WorkgroupMemory); | |
| } | |
| if (semantics & dawn::to_underlying(spv::MemorySemanticsMask::UniformMemory)) { | |
| EmitWithoutSpvResult(b_.Call(ty_.void_(), core::BuiltinFn::kStorageBarrier)); | |
| semantics &= ~dawn::to_underlying(spv::MemorySemanticsMask::UniformMemory); | |
| } | |
| if (semantics & dawn::to_underlying(spv::MemorySemanticsMask::ImageMemory)) { | |
| EmitWithoutSpvResult(b_.Call(ty_.void_(), core::BuiltinFn::kTextureBarrier)); | |
| semantics &= ~dawn::to_underlying(spv::MemorySemanticsMask::ImageMemory); | |
| } | |
| if (semantics != 0) { | |
| return Failure("unsupported control barrier semantics: " + std::to_string(semantics)); | |
| } | |
| return Success; | |
| } | |
| void CheckAtomicNotFloat(const spvtools::opt::Instruction& inst) { | |
| auto* ty = Type(inst.type_id()); | |
| TINT_ASSERT(!ty->UnwrapPtr()->IsFloatScalar()) | |
| << "Atomic operations on floating point values not supported."; | |
| } | |
| void EmitAtomicStore(const spvtools::opt::Instruction& inst) { | |
| auto* v = Value(inst.GetSingleWordInOperand(0)); | |
| auto* ty = v->Type()->UnwrapPtr(); | |
| TINT_ASSERT(!ty->IsFloatScalar()) | |
| << "Atomic operations on floating point values not supported."; | |
| EmitWithoutSpvResult(b_.Call<spirv::ir::BuiltinCall>( | |
| ty_.void_(), spirv::BuiltinFn::kAtomicStore, Args(inst, 0)) | |
| ->Result()); | |
| } | |
| void EmitBitcast(const spvtools::opt::Instruction& inst) { | |
| auto val = Value(inst.GetSingleWordInOperand(0)); | |
| auto ty = Type(inst.type_id()); | |
| EmitOrAdd(b_.Bitcast(ty, val), inst.result_id()); | |
| } | |
| core::ir::ControlInstruction* StopWalkingAt(uint32_t id) { | |
| auto iter = walk_stop_blocks_.find(id); | |
| if (iter != walk_stop_blocks_.end()) { | |
| return iter->second; | |
| } | |
| return nullptr; | |
| } | |
| core::ir::Loop* ContinueTarget(uint32_t id) { | |
| auto iter = continue_targets_.find(id); | |
| if (iter != continue_targets_.end()) { | |
| return iter->second; | |
| } | |
| return nullptr; | |
| } | |
| Result<SuccessType> EmitBranch(spvtools::opt::Instruction& inst) { | |
| auto dest_id = inst.GetSingleWordInOperand(0); | |
| // Disallow fallthrough | |
| for (auto& switch_blocks : current_switch_blocks_) { | |
| if (switch_blocks.count(dest_id) != 0) { | |
| return Failure("switch fallthrough not supported by the SPIR-V reader"); | |
| } | |
| } | |
| // The destination is a continuing block, so insert a `continue` | |
| if (auto* loop = ContinueTarget(dest_id)) { | |
| auto* new_inst = b_.Continue(loop); | |
| auto id = spirv_context_->get_instr_block(&inst)->id(); | |
| inst_to_spirv_block_[new_inst] = id; | |
| EmitWithoutResult(new_inst); | |
| return Success; | |
| } | |
| // If this is branching to a previous merge block then we're done. It can be a previous | |
| // merge block in the case of an `if` breaking out of a `switch` or `loop`. | |
| if (auto* ctrl_inst = StopWalkingAt(dest_id)) { | |
| if (auto* loop = ctrl_inst->As<core::ir::Loop>()) { | |
| // Going to the merge in a loop body has to be a break regardless of nesting level. | |
| if (InBlock(loop->Body()) && !InBlock(loop->Continuing())) { | |
| EmitWithoutResult(b_.Exit(ctrl_inst)); | |
| } | |
| } else if (ctrl_inst->Is<core::ir::Switch>()) { | |
| EmitWithoutResult(b_.Exit(ctrl_inst)); | |
| } | |
| return Success; | |
| } | |
| TINT_ASSERT(current_spirv_function_); | |
| const auto& bb = current_spirv_function_->FindBlock(dest_id); | |
| TINT_CHECK_RESULT(EmitBlock(current_block_, *bb)); | |
| return Success; | |
| } | |
| // Given a true and false branch find if there is a common convergence point before the merge | |
| // block. | |
| std::optional<uint32_t> FindPremergeId(uint32_t true_id, | |
| uint32_t false_id, | |
| std::optional<uint32_t> merge_id) { | |
| auto* cfg = spirv_context_->cfg(); | |
| // We need a merge block, the true and false to be unique and the true and false to not be | |
| // the merge. | |
| if (!merge_id || true_id == false_id || true_id == merge_id || false_id == merge_id) { | |
| return std::nullopt; | |
| } | |
| // Get the list of blocks from the true branch to the merge | |
| std::list<spvtools::opt::BasicBlock*> true_blocks; | |
| cfg->ComputeStructuredOrder( | |
| current_spirv_function_, &*(current_spirv_function_->FindBlock(true_id)), | |
| &*(current_spirv_function_->FindBlock(merge_id.value())), &true_blocks); | |
| // Get the list of blocks from the false branch to the merge | |
| std::list<spvtools::opt::BasicBlock*> false_blocks; | |
| cfg->ComputeStructuredOrder( | |
| current_spirv_function_, &*(current_spirv_function_->FindBlock(false_id)), | |
| &*(current_spirv_function_->FindBlock(merge_id.value())), &false_blocks); | |
| auto& true_end = true_blocks.back(); | |
| auto& false_end = false_blocks.back(); | |
| // We only consider the block as returning if it didn't return through | |
| // the merge block. (I.e. it's a direct exit from inside the branch | |
| // itself. | |
| bool true_returns = true_end->id() != merge_id && true_end->IsReturn(); | |
| bool false_returns = false_end->id() != merge_id && false_end->IsReturn(); | |
| // If one of the blocks returns but the other doesn't, then we can't | |
| // have a premerge block. | |
| if (true_returns != false_returns) { | |
| return std::nullopt; | |
| } | |
| // If they don't return, both blocks must merge to the same place. | |
| if (!true_returns && (true_end->id() != false_end->id())) { | |
| return std::nullopt; | |
| } | |
| // If these aren't returns, then remove the merge blocks. | |
| if (!true_returns) { | |
| true_blocks.pop_back(); | |
| false_blocks.pop_back(); | |
| } | |
| std::optional<uint32_t> id = std::nullopt; | |
| while (!true_blocks.empty() && !false_blocks.empty()) { | |
| auto* tb = true_blocks.back(); | |
| if (tb != false_blocks.back()) { | |
| break; | |
| } | |
| id = tb->id(); | |
| true_blocks.pop_back(); | |
| false_blocks.pop_back(); | |
| } | |
| // If this is already a stop block, so it can't be a premerge | |
| if (id.has_value() && walk_stop_blocks_.contains(id.value())) { | |
| return std::nullopt; | |
| } | |
| return id; | |
| } | |
| core::ir::ControlInstruction* ExitFor(core::ir::ControlInstruction* ctrl, | |
| core::ir::ControlInstruction* parent) { | |
| // If you have a BranchConditional inside a BranchConditional where | |
| // the inner does not have a merge block, it can branch out to the | |
| // merge of the outer conditional. But, WGSL doesn't allow that, so | |
| // just treat it as an exit of the inner block. | |
| if (ctrl->Is<core::ir::If>() && parent->Is<core::ir::If>()) { | |
| return parent; | |
| } | |
| return ctrl; | |
| } | |
| core::ir::Instruction* EmitBranchStopBlock(core::ir::ControlInstruction* ctrl, | |
| core::ir::If* if_, | |
| core::ir::Block* blk, | |
| uint32_t target) { | |
| if (auto* loop = ContinueTarget(target)) { | |
| auto* cont = b_.Continue(loop); | |
| blk->Append(cont); | |
| return cont; | |
| } | |
| auto iter = merge_to_premerge_.find(target); | |
| if (iter != merge_to_premerge_.end()) { | |
| // Branch to a merge block, but skipping over an expected premerge block | |
| // so we need a guard. | |
| if (!iter->second.condition) { | |
| b_.InsertBefore(iter->second.parent, | |
| [&] { iter->second.condition = b_.Var("execute_premerge", true); }); | |
| } | |
| b_.Append(blk, [&] { b_.Store(iter->second.condition, false); }); | |
| } | |
| auto* exit = b_.Exit(ExitFor(ctrl, if_)); | |
| blk->Append(exit); | |
| return exit; | |
| } | |
| bool ProcessBranchAsLoopHeader(core::ir::Value* cond, uint32_t true_id, uint32_t false_id) { | |
| bool true_is_header = loop_headers_.count(true_id) > 0; | |
| bool false_is_header = loop_headers_.count(false_id) > 0; | |
| if (!true_is_header && !false_is_header) { | |
| return false; | |
| } | |
| core::ir::Loop* loop = nullptr; | |
| uint32_t merge_id = 0; | |
| if (true_is_header) { | |
| const auto& bb_header = current_spirv_function_->FindBlock(true_id); | |
| merge_id = (*bb_header).MergeBlockIdIfAny(); | |
| loop = loop_headers_[true_id]; | |
| } else { | |
| const auto& bb_header = current_spirv_function_->FindBlock(false_id); | |
| merge_id = (*bb_header).MergeBlockIdIfAny(); | |
| loop = loop_headers_[false_id]; | |
| } | |
| TINT_ASSERT(merge_id > 0); | |
| // The only time a loop continuing will be in current blocks is if | |
| // we're inside the continuing block itself. | |
| // | |
| // Note, we may _not_ be in the IR continuing block. This can happen | |
| // in the case of a SPIR-V loop where the header_id and continue_id | |
| // are the same. We'll be emitting into the IR body, but branch to | |
| // the header because that's also the continuing in SPIR-V. | |
| if (current_blocks_.count(loop->Continuing()) != 0u) { | |
| if (true_id == merge_id && false_is_header) { | |
| EmitWithoutResult(b_.BreakIf(loop, cond)); | |
| return true; | |
| } | |
| if (false_id == merge_id && true_is_header) { | |
| auto* val = b_.Not(cond); | |
| if (auto* inst = val->AsInstruction()) { | |
| EmitWithoutSpvResult(inst->Result()); | |
| } | |
| EmitWithoutResult(b_.BreakIf(loop, val)); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| Result<SuccessType> EmitPremergeBlock(uint32_t merge_id, | |
| uint32_t premerge_start_id, | |
| core::ir::If* premerge_if_) { | |
| auto iter = merge_to_premerge_.find(merge_id); | |
| TINT_ASSERT(iter != merge_to_premerge_.end()); | |
| // If we created a condition guard, we need to swap the premerge `true` condition with | |
| // the condition variable. | |
| if (iter->second.condition) { | |
| auto* premerge_cond = b_.Load(iter->second.condition); | |
| EmitWithoutSpvResult(premerge_cond->Result()); | |
| premerge_if_->SetOperand(core::ir::If::kConditionOperandOffset, | |
| premerge_cond->Result()); | |
| } | |
| merge_to_premerge_.erase(iter); | |
| EmitWithoutResult(premerge_if_); | |
| const auto& bb_premerge = current_spirv_function_->FindBlock(premerge_start_id); | |
| TINT_CHECK_RESULT(EmitBlockParent(premerge_if_->True(), *bb_premerge)); | |
| if (!premerge_if_->True()->Terminator()) { | |
| premerge_if_->True()->Append(b_.Exit(premerge_if_)); | |
| } | |
| premerge_if_->False()->Append(b_.Unreachable()); | |
| return Success; | |
| } | |
| Result<SuccessType> EmitIfBranch(uint32_t id, core::ir::If* if_, core::ir::Block* blk) { | |
| const auto& bb = current_spirv_function_->FindBlock(id); | |
| TINT_CHECK_RESULT(EmitBlockParent(blk, *bb)); | |
| if (!blk->Terminator()) { | |
| blk->Append(b_.Exit(if_)); | |
| } | |
| return Success; | |
| } | |
| Result<SuccessType> EmitBranchConditional(const spvtools::opt::BasicBlock& bb, | |
| const spvtools::opt::Instruction& inst) { | |
| auto cond = Value(inst.GetSingleWordInOperand(0)); | |
| auto true_id = inst.GetSingleWordInOperand(1); | |
| auto false_id = inst.GetSingleWordInOperand(2); | |
| if (ProcessBranchAsLoopHeader(cond, true_id, false_id)) { | |
| return Success; | |
| } | |
| // If the true and false block are the same, then we change the condition into | |
| // `cond || true` so that we always take the true block, the false block will be marked | |
| // unreachable. | |
| if (true_id == false_id) { | |
| auto* binary = b_.Binary(core::BinaryOp::kOr, cond->Type(), cond, b_.Constant(true)); | |
| EmitWithoutSpvResult(binary); | |
| cond = binary; | |
| } | |
| auto* if_ = b_.If(cond); | |
| EmitWithoutResult(if_); | |
| branch_conditional_to_if_.insert({&inst, if_}); | |
| std::optional<uint32_t> merge_id = std::nullopt; | |
| auto* merge_inst = bb.GetMergeInst(); | |
| if (bb.GetLoopMergeInst()) { | |
| // If this is a loop merge block, then the merge instruction is for | |
| // the loop, not the branch conditional. | |
| merge_inst = nullptr; | |
| } else if (merge_inst != nullptr) { | |
| merge_id = merge_inst->GetSingleWordInOperand(0); | |
| walk_stop_blocks_.insert({merge_id.value(), if_}); | |
| } | |
| TINT_ASSERT(current_spirv_function_); | |
| // Determine if there is a premerge block to handle | |
| std::optional<uint32_t> premerge_start_id = FindPremergeId(true_id, false_id, merge_id); | |
| // If we found the start of a premerge, push it onto the merge stack so this ends up being a | |
| // temporary merge block for the if branches. | |
| core::ir::If* premerge_if_ = nullptr; | |
| if (premerge_start_id.has_value()) { | |
| // Must have a merge to have a premerge | |
| merge_to_premerge_.insert({merge_id.value(), PremergeInfo{if_, {}}}); | |
| premerge_if_ = b_.If(b_.Constant(true)); | |
| walk_stop_blocks_.insert({premerge_start_id.value(), premerge_if_}); | |
| } | |
| if (auto* ctrl = StopWalkingAt(true_id)) { | |
| auto* new_inst = EmitBranchStopBlock(ctrl, if_, if_->True(), true_id); | |
| inst_to_spirv_block_[new_inst] = bb.id(); | |
| } else { | |
| TINT_CHECK_RESULT(EmitIfBranch(true_id, if_, if_->True())); | |
| } | |
| // Pre-SPIRV 1.6 the true and false blocks could be the same. If that's the case then we | |
| // will have changed the condition and the false block is now unreachable. | |
| if (false_id == true_id) { | |
| if_->False()->Append(b_.Unreachable()); | |
| } else if (auto* ctrl = StopWalkingAt(false_id)) { | |
| auto* new_inst = EmitBranchStopBlock(ctrl, if_, if_->False(), false_id); | |
| inst_to_spirv_block_[new_inst] = bb.id(); | |
| } else { | |
| TINT_CHECK_RESULT(EmitIfBranch(false_id, if_, if_->False())); | |
| } | |
| // There was a premerge, remove it from the merge stack and then emit the premerge into an | |
| // `if true` block in order to maintain re-convergence guarantees. The premerge will contain | |
| // all the blocks up to the merge block. | |
| if (premerge_start_id.has_value()) { | |
| TINT_CHECK_RESULT( | |
| EmitPremergeBlock(merge_id.value(), premerge_start_id.value(), premerge_if_)); | |
| } | |
| // Emit the merge block if it exists. | |
| if (merge_id.has_value()) { | |
| const auto& bb_merge = current_spirv_function_->FindBlock(merge_id.value()); | |
| TINT_CHECK_RESULT(EmitBlock(current_block_, *bb_merge)); | |
| } | |
| return Success; | |
| } | |
| void EmitLoop(const spvtools::opt::BasicBlock& bb) { | |
| // This just handles creating the loop itself, the rest of the processing | |
| // of the continue and merge blocks will be handled when we deal with the | |
| // LoopMerge instruction itself. We have to setup the loop early in order | |
| // to capture instructions which come in the header before the LoopMerge. | |
| auto* loop = b_.Loop(); | |
| EmitWithoutResult(loop); | |
| // A `loop` header block can also be the merge block for an `if`. In that the case, replace | |
| // the `if` information in the stop blocks with the loop as this must be the `if` merge | |
| // block and the `if` is complete. | |
| walk_stop_blocks_[bb.id()] = loop; | |
| } | |
| void EmitLoopMerge(const spvtools::opt::BasicBlock& bb, | |
| const spvtools::opt::Instruction& inst) { | |
| auto merge_id = inst.GetSingleWordInOperand(0); | |
| auto continue_id = inst.GetSingleWordInOperand(1); | |
| auto header_id = bb.id(); | |
| // The loop was created in `EmitLoop` and set as the stop block value for | |
| // the header block. Retrieve the loop from the stop list. | |
| auto* loop = StopWalkingAt(header_id)->As<core::ir::Loop>(); | |
| TINT_ASSERT(loop); | |
| loop_headers_.insert({header_id, loop}); | |
| continue_targets_.insert({continue_id, loop}); | |
| // Insert the stop blocks | |
| walk_stop_blocks_.insert({merge_id, loop}); | |
| if (continue_id != header_id) { | |
| walk_stop_blocks_.insert({continue_id, loop}); | |
| } | |
| // The remainder of the loop body will process when we hit the | |
| // BranchConditional or Branch after the LoopMerge. We're already | |
| // processing into the loop body from the `EmitLoop` code above so just | |
| // continue emitting. | |
| // The merge block will be emitted by the `EmitBlock` code after the | |
| // instructions in the loop header are emitted. | |
| } | |
| Result<SuccessType> EmitSwitch(const spvtools::opt::BasicBlock& bb, | |
| const spvtools::opt::Instruction& inst) { | |
| auto* selector = Value(inst.GetSingleWordInOperand(0)); | |
| auto default_id = inst.GetSingleWordInOperand(1); | |
| auto* switch_ = b_.Switch(selector); | |
| EmitWithoutResult(switch_); | |
| auto* merge_inst = bb.GetMergeInst(); | |
| TINT_ASSERT(merge_inst); | |
| auto merge_id = merge_inst->GetSingleWordInOperand(0); | |
| walk_stop_blocks_.insert({merge_id, switch_}); | |
| size_t switch_blocks_id = current_switch_blocks_.size(); | |
| current_switch_blocks_.push_back({}); | |
| auto* default_blk = b_.DefaultCase(switch_); | |
| if (default_id != merge_id) { | |
| current_switch_blocks_[switch_blocks_id].emplace(default_id); | |
| const auto& bb_default = current_spirv_function_->FindBlock(default_id); | |
| TINT_CHECK_RESULT(EmitBlockParent(default_blk, *bb_default)); | |
| } | |
| if (!default_blk->Terminator()) { | |
| default_blk->Append(b_.ExitSwitch(switch_)); | |
| } | |
| std::unordered_map<uint32_t, size_t> block_id_to_case_index; | |
| block_id_to_case_index[default_id] = switch_->Cases().Length() - 1; | |
| for (uint32_t i = 2; i < inst.NumInOperandWords(); i += 2) { | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| if (blk_id != merge_id) { | |
| current_switch_blocks_[switch_blocks_id].emplace(blk_id); | |
| } | |
| } | |
| // For each selector. | |
| for (uint32_t i = 2; i < inst.NumInOperandWords(); i += 2) { | |
| auto literal = inst.GetSingleWordInOperand(i); | |
| auto blk_id = inst.GetSingleWordInOperand(i + 1); | |
| core::ir::Constant* sel = nullptr; | |
| if (selector->Type()->Is<core::type::I32>()) { | |
| sel = b_.Constant(i32(literal)); | |
| } else { | |
| sel = b_.Constant(u32(literal)); | |
| } | |
| // Determine if we've seen this block and should combine selectors | |
| auto case_index_iter = block_id_to_case_index.find(blk_id); | |
| if (case_index_iter != block_id_to_case_index.end()) { | |
| switch_->Cases()[case_index_iter->second].selectors.Push( | |
| core::ir::Switch::CaseSelector{sel}); | |
| continue; | |
| } | |
| core::ir::Block* blk = b_.Case(switch_, Vector{sel}); | |
| if (blk_id != merge_id) { | |
| const auto& basic_block = current_spirv_function_->FindBlock(blk_id); | |
| TINT_CHECK_RESULT(EmitBlockParent(blk, *basic_block)); | |
| } | |
| if (!blk->Terminator()) { | |
| blk->Append(b_.ExitSwitch(switch_)); | |
| } | |
| block_id_to_case_index[blk_id] = switch_->Cases().Length() - 1; | |
| } | |
| current_switch_blocks_.pop_back(); | |
| const auto& bb_merge = current_spirv_function_->FindBlock(merge_id); | |
| TINT_CHECK_RESULT(EmitBlock(current_block_, *bb_merge)); | |
| return Success; | |
| } | |
| Vector<core::ir::Value*, 4> Args(const spvtools::opt::Instruction& inst, uint32_t start) { | |
| Vector<core::ir::Value*, 4> args; | |
| for (uint32_t i = start; i < inst.NumOperandWords(); i++) { | |
| args.Push(Value(inst.GetSingleWordOperand(i))); | |
| } | |
| return args; | |
| } | |
| void EmitBuiltinCall(const spvtools::opt::Instruction& inst, core::BuiltinFn fn) { | |
| EmitOrAdd(b_.Call(Type(inst.type_id()), fn, Args(inst, 2)), inst.result_id()); | |
| } | |
| void EmitSpirvExplicitBuiltinCall(const spvtools::opt::Instruction& inst, | |
| spirv::BuiltinFn fn, | |
| uint32_t first_operand_idx = 2) { | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>( | |
| Type(inst.type_id()), fn, | |
| Vector<core::ir::TemplateParameter, 1>{Type(inst.type_id())->DeepestElement()}, | |
| Args(inst, first_operand_idx)), | |
| inst.result_id()); | |
| } | |
| void EmitSpirvBuiltinCall(const spvtools::opt::Instruction& inst, | |
| spirv::BuiltinFn fn, | |
| uint32_t first_operand_idx = 2) { | |
| Emit(b_.Call<spirv::ir::BuiltinCall>(Type(inst.type_id()), fn, | |
| Args(inst, first_operand_idx)), | |
| inst.result_id()); | |
| } | |
| void EmitBitCount(const spvtools::opt::Instruction& inst) { | |
| auto* res_ty = Type(inst.type_id()); | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>( | |
| res_ty, spirv::BuiltinFn::kBitCount, | |
| Vector<core::ir::TemplateParameter, 1>{res_ty->DeepestElement()}, Args(inst, 2)), | |
| inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction | |
| /// Note: This isn't technically correct, but there is no `kill` equivalent in WGSL. The closets | |
| /// we have is `discard` which maps to `OpDemoteToHelperInvocation` in SPIR-V. | |
| void EmitKill([[maybe_unused]] const spvtools::opt::Instruction& inst) { | |
| // TODO(430084563): Log a warning that the `OpKill` or `OpTerminateInvocation` will not have | |
| // the same semantics as the original SPIR-V. | |
| EmitWithoutResult(b_.Discard()); | |
| // An `OpKill` is a terminator in SPIR-V. `discard` is not a terminator in WGSL. After the | |
| // `discard` we inject a `return` for the current function. This is similar in spirit to | |
| // what `OpKill` does although not totally correct (i.e. we don't early return from calling | |
| // functions, just the function where `OpKill` was emitted. There are also limited places in | |
| // which `OpKill` can be used. So, we don't have to worry about it in a `continuing` block | |
| // because the continuing must end with a branching terminator which `OpKill` does not | |
| // branch. | |
| if (current_function_->ReturnType()->Is<core::type::Void>()) { | |
| EmitWithoutResult(b_.Return(current_function_)); | |
| } else { | |
| EmitWithoutResult( | |
| b_.Return(current_function_, b_.Zero(current_function_->ReturnType()))); | |
| } | |
| } | |
| /// @param inst the SPIR-V instruction for OpCopyObject | |
| void EmitCopyObject(const spvtools::opt::Instruction& inst) { | |
| // Make the result Id a pointer to the original copied value. | |
| auto* v = Value(inst.GetSingleWordOperand(2)); | |
| if (v->Type()->IsHandle() || v->Type()->Is<spirv::type::SampledImage>()) { | |
| values_.Add(inst.result_id(), v); | |
| } else { | |
| auto* l = b_.Let(v); | |
| Emit(l, inst.result_id()); | |
| } | |
| } | |
| /// @param inst the SPIR-V instruction for OpCopyMemory | |
| void EmitCopyMemory(const spvtools::opt::Instruction& inst) { | |
| auto load = b_.Load(Value(inst.GetSingleWordOperand(1))); | |
| EmitWithoutSpvResult(load->Result()); | |
| EmitWithoutResult(b_.Store(Value(inst.GetSingleWordOperand(0)), load)); | |
| } | |
| /// @param inst the SPIR-V instruction for OpExtInst | |
| Result<SuccessType> EmitExtInst(const spvtools::opt::Instruction& inst) { | |
| auto inst_set = inst.GetSingleWordInOperand(0); | |
| if (ignored_imports_.count(inst_set) > 0) { | |
| // Ignore it but don't error out. | |
| return Success; | |
| } | |
| if (glsl_std_450_imports_.count(inst_set) > 0) { | |
| return EmitGlslStd450ExtInst(inst); | |
| } | |
| TINT_UNREACHABLE() | |
| << "unrecognized extended instruction set should have been caught during registration"; | |
| } | |
| // Returns the WGSL standard library function for the given GLSL.std.450 extended instruction | |
| // operation code. This handles GLSL functions which directly translate to the WGSL equivalent. | |
| // Any non-direct translation is returned as `kNone`. | |
| core::BuiltinFn GetGlslStd450WgslEquivalentFuncName(uint32_t ext_opcode) { | |
| switch (ext_opcode) { | |
| case GLSLstd450Acos: | |
| return core::BuiltinFn::kAcos; | |
| case GLSLstd450Acosh: | |
| return core::BuiltinFn::kAcosh; | |
| case GLSLstd450Asin: | |
| return core::BuiltinFn::kAsin; | |
| case GLSLstd450Asinh: | |
| return core::BuiltinFn::kAsinh; | |
| case GLSLstd450Atan: | |
| return core::BuiltinFn::kAtan; | |
| case GLSLstd450Atanh: | |
| return core::BuiltinFn::kAtanh; | |
| case GLSLstd450Atan2: | |
| return core::BuiltinFn::kAtan2; | |
| case GLSLstd450Ceil: | |
| return core::BuiltinFn::kCeil; | |
| case GLSLstd450Cos: | |
| return core::BuiltinFn::kCos; | |
| case GLSLstd450Cosh: | |
| return core::BuiltinFn::kCosh; | |
| case GLSLstd450Cross: | |
| return core::BuiltinFn::kCross; | |
| case GLSLstd450Degrees: | |
| return core::BuiltinFn::kDegrees; | |
| case GLSLstd450Determinant: | |
| return core::BuiltinFn::kDeterminant; | |
| case GLSLstd450Distance: | |
| return core::BuiltinFn::kDistance; | |
| case GLSLstd450Exp: | |
| return core::BuiltinFn::kExp; | |
| case GLSLstd450Exp2: | |
| return core::BuiltinFn::kExp2; | |
| case GLSLstd450FAbs: | |
| return core::BuiltinFn::kAbs; | |
| case GLSLstd450FSign: | |
| return core::BuiltinFn::kSign; | |
| case GLSLstd450Floor: | |
| return core::BuiltinFn::kFloor; | |
| case GLSLstd450Fract: | |
| return core::BuiltinFn::kFract; | |
| case GLSLstd450Fma: | |
| return core::BuiltinFn::kFma; | |
| case GLSLstd450InverseSqrt: | |
| return core::BuiltinFn::kInverseSqrt; | |
| case GLSLstd450Length: | |
| return core::BuiltinFn::kLength; | |
| case GLSLstd450Log: | |
| return core::BuiltinFn::kLog; | |
| case GLSLstd450Log2: | |
| return core::BuiltinFn::kLog2; | |
| case GLSLstd450NClamp: | |
| case GLSLstd450FClamp: // FClamp is less prescriptive about NaN operands | |
| return core::BuiltinFn::kClamp; | |
| case GLSLstd450ModfStruct: | |
| return core::BuiltinFn::kModf; | |
| case GLSLstd450FrexpStruct: | |
| return core::BuiltinFn::kFrexp; | |
| case GLSLstd450NMin: | |
| case GLSLstd450FMin: // FMin is less prescriptive about NaN operands | |
| return core::BuiltinFn::kMin; | |
| case GLSLstd450NMax: | |
| case GLSLstd450FMax: // FMax is less prescriptive about NaN operands | |
| return core::BuiltinFn::kMax; | |
| case GLSLstd450FMix: | |
| return core::BuiltinFn::kMix; | |
| case GLSLstd450PackSnorm4x8: | |
| return core::BuiltinFn::kPack4X8Snorm; | |
| case GLSLstd450PackUnorm4x8: | |
| return core::BuiltinFn::kPack4X8Unorm; | |
| case GLSLstd450PackSnorm2x16: | |
| return core::BuiltinFn::kPack2X16Snorm; | |
| case GLSLstd450PackUnorm2x16: | |
| return core::BuiltinFn::kPack2X16Unorm; | |
| case GLSLstd450PackHalf2x16: | |
| return core::BuiltinFn::kPack2X16Float; | |
| case GLSLstd450Pow: | |
| return core::BuiltinFn::kPow; | |
| case GLSLstd450Radians: | |
| return core::BuiltinFn::kRadians; | |
| case GLSLstd450Round: | |
| case GLSLstd450RoundEven: | |
| return core::BuiltinFn::kRound; | |
| case GLSLstd450Sin: | |
| return core::BuiltinFn::kSin; | |
| case GLSLstd450Sinh: | |
| return core::BuiltinFn::kSinh; | |
| case GLSLstd450SmoothStep: | |
| return core::BuiltinFn::kSmoothstep; | |
| case GLSLstd450Sqrt: | |
| return core::BuiltinFn::kSqrt; | |
| case GLSLstd450Step: | |
| return core::BuiltinFn::kStep; | |
| case GLSLstd450Tan: | |
| return core::BuiltinFn::kTan; | |
| case GLSLstd450Tanh: | |
| return core::BuiltinFn::kTanh; | |
| case GLSLstd450Trunc: | |
| return core::BuiltinFn::kTrunc; | |
| case GLSLstd450UnpackSnorm4x8: | |
| return core::BuiltinFn::kUnpack4X8Snorm; | |
| case GLSLstd450UnpackUnorm4x8: | |
| return core::BuiltinFn::kUnpack4X8Unorm; | |
| case GLSLstd450UnpackSnorm2x16: | |
| return core::BuiltinFn::kUnpack2X16Snorm; | |
| case GLSLstd450UnpackUnorm2x16: | |
| return core::BuiltinFn::kUnpack2X16Unorm; | |
| case GLSLstd450UnpackHalf2x16: | |
| return core::BuiltinFn::kUnpack2X16Float; | |
| default: | |
| break; | |
| } | |
| return core::BuiltinFn::kNone; | |
| } | |
| spirv::BuiltinFn GetGlslStd450SpirvEquivalentFuncName(uint32_t ext_opcode) { | |
| switch (ext_opcode) { | |
| case GLSLstd450SAbs: | |
| return spirv::BuiltinFn::kAbs; | |
| case GLSLstd450SSign: | |
| return spirv::BuiltinFn::kSign; | |
| case GLSLstd450Normalize: | |
| return spirv::BuiltinFn::kNormalize; | |
| case GLSLstd450MatrixInverse: | |
| return spirv::BuiltinFn::kInverse; | |
| case GLSLstd450SMax: | |
| return spirv::BuiltinFn::kSMax; | |
| case GLSLstd450SMin: | |
| return spirv::BuiltinFn::kSMin; | |
| case GLSLstd450SClamp: | |
| return spirv::BuiltinFn::kSClamp; | |
| case GLSLstd450UMax: | |
| return spirv::BuiltinFn::kUMax; | |
| case GLSLstd450UMin: | |
| return spirv::BuiltinFn::kUMin; | |
| case GLSLstd450UClamp: | |
| return spirv::BuiltinFn::kUClamp; | |
| case GLSLstd450FindILsb: | |
| return spirv::BuiltinFn::kFindILsb; | |
| case GLSLstd450FindSMsb: | |
| return spirv::BuiltinFn::kFindSMsb; | |
| case GLSLstd450FindUMsb: | |
| return spirv::BuiltinFn::kFindUMsb; | |
| case GLSLstd450Refract: | |
| return spirv::BuiltinFn::kRefract; | |
| case GLSLstd450Reflect: | |
| return spirv::BuiltinFn::kReflect; | |
| case GLSLstd450FaceForward: | |
| return spirv::BuiltinFn::kFaceForward; | |
| case GLSLstd450Ldexp: | |
| return spirv::BuiltinFn::kLdexp; | |
| case GLSLstd450Modf: | |
| return spirv::BuiltinFn::kModf; | |
| case GLSLstd450Frexp: | |
| return spirv::BuiltinFn::kFrexp; | |
| default: | |
| break; | |
| } | |
| return spirv::BuiltinFn::kNone; | |
| } | |
| Vector<core::ir::TemplateParameter, 1> GlslStd450ExplicitParams( | |
| uint32_t ext_opcode, | |
| const core::type::Type* result_ty) { | |
| if (ext_opcode == GLSLstd450SSign || ext_opcode == GLSLstd450SAbs || | |
| ext_opcode == GLSLstd450SMax || ext_opcode == GLSLstd450SMin || | |
| ext_opcode == GLSLstd450SClamp || ext_opcode == GLSLstd450UMax || | |
| ext_opcode == GLSLstd450UMin || ext_opcode == GLSLstd450UClamp || | |
| ext_opcode == GLSLstd450FindILsb || ext_opcode == GLSLstd450FindSMsb || | |
| ext_opcode == GLSLstd450FindUMsb) { | |
| return {result_ty->DeepestElement()}; | |
| } | |
| return {}; | |
| } | |
| /// @param inst the SPIR-V instruction for OpAccessChain | |
| Result<SuccessType> EmitGlslStd450ExtInst(const spvtools::opt::Instruction& inst) { | |
| const auto ext_opcode = inst.GetSingleWordInOperand(1); | |
| auto* spv_ty = Type(inst.type_id()); | |
| Vector<core::ir::Value*, 4> operands; | |
| // All parameters to GLSL.std.450 extended instructions are IDs. | |
| for (uint32_t idx = 2; idx < inst.NumInOperands(); ++idx) { | |
| operands.Push(Value(inst.GetSingleWordInOperand(idx))); | |
| } | |
| const auto wgsl_fn = GetGlslStd450WgslEquivalentFuncName(ext_opcode); | |
| if (wgsl_fn == core::BuiltinFn::kModf) { | |
| // For `ModfStruct`, which is, essentially, a WGSL `modf` instruction | |
| // we need some special handling. The result type that we produce | |
| // must be the SPIR-V type as we don't know how the result is used | |
| // later. So, we need to make the WGSL query and re-construct an | |
| // object of the right SPIR-V type. We can't, easily, do this later | |
| // as we lose the SPIR-V type as soon as we replace the result of the | |
| // `modf`. So, inline the work here to generate the correct results. | |
| auto* mem_ty = operands[0]->Type(); | |
| auto* result_ty = core::type::CreateModfResult(ty_, ir_.symbols, mem_ty); | |
| auto* call = b_.Call(result_ty, wgsl_fn, operands); | |
| auto* fract = b_.Access(mem_ty, call, 0_u); | |
| auto* whole = b_.Access(mem_ty, call, 1_u); | |
| EmitWithoutSpvResult(call); | |
| EmitWithoutSpvResult(fract); | |
| EmitWithoutSpvResult(whole); | |
| EmitOrAdd(b_.Construct(spv_ty, fract, whole), inst.result_id()); | |
| return Success; | |
| } | |
| if (wgsl_fn == core::BuiltinFn::kFrexp) { | |
| // For `FrexpStruct`, which is, essentially, a WGSL `frexp` | |
| // instruction we need some special handling. The result type that we | |
| // produce must be the SPIR-V type as we don't know how the result is | |
| // used later. So, we need to make the WGSL query and re-construct an | |
| // object of the right SPIR-V type. We can't, easily, do this later | |
| // as we lose the SPIR-V type as soon as we replace the result of the | |
| // `frexp`. So, inline the work here to generate the correct results. | |
| auto* mem_ty = operands[0]->Type(); | |
| auto* result_ty = core::type::CreateFrexpResult(ty_, ir_.symbols, mem_ty); | |
| auto* call = b_.Call(result_ty, wgsl_fn, operands); | |
| auto* fract = b_.Access(mem_ty, call, 0_u); | |
| auto* exp = b_.Access(ty_.MatchWidth(ty_.i32(), mem_ty), call, 1_u); | |
| core::ir::Value* exp_res = exp; | |
| EmitWithoutSpvResult(call); | |
| EmitWithoutSpvResult(fract); | |
| EmitWithoutSpvResult(exp); | |
| if (auto* str = spv_ty->As<core::type::Struct>()) { | |
| auto* exp_ty = str->Members()[1]->Type(); | |
| if (exp_ty->DeepestElement()->IsUnsignedIntegerScalar()) { | |
| exp_res = b_.Bitcast(exp_ty, exp); | |
| EmitWithoutSpvResult(exp_res); | |
| } | |
| } | |
| EmitOrAdd(b_.Construct(spv_ty, fract, exp_res), inst.result_id()); | |
| return Success; | |
| } | |
| if (wgsl_fn != core::BuiltinFn::kNone) { | |
| EmitOrAdd(b_.Call(spv_ty, wgsl_fn, operands), inst.result_id()); | |
| return Success; | |
| } | |
| const auto spv_fn = GetGlslStd450SpirvEquivalentFuncName(ext_opcode); | |
| if (spv_fn != spirv::BuiltinFn::kNone) { | |
| auto explicit_params = GlslStd450ExplicitParams(ext_opcode, spv_ty); | |
| Emit(b_.CallExplicit<spirv::ir::BuiltinCall>(spv_ty, spv_fn, explicit_params, operands), | |
| inst.result_id()); | |
| return Success; | |
| } | |
| TINT_UNREACHABLE() << "unhandled GLSL.std.450 instruction " << ext_opcode; | |
| } | |
| /// @param inst the SPIR-V instruction for OpAccessChain | |
| void EmitAccess(const spvtools::opt::Instruction& inst) { | |
| Vector indices = Args(inst, 3); | |
| auto* base = Value(inst.GetSingleWordOperand(2)); | |
| if (indices.IsEmpty()) { | |
| // There are no indices, so just forward the base object. | |
| AddValue(inst.result_id(), base); | |
| return; | |
| } | |
| // Propagate the access mode of the base object. | |
| auto access_mode = core::Access::kUndefined; | |
| if (auto* ptr = base->Type()->As<core::type::Pointer>()) { | |
| access_mode = ptr->Access(); | |
| } | |
| auto* access = b_.Access(Type(inst.type_id(), access_mode), base, std::move(indices)); | |
| EmitOrAdd(access, inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction | |
| /// @param op the unary operator to use | |
| void EmitUnary(const spvtools::opt::Instruction& inst, | |
| core::UnaryOp op, | |
| uint32_t first_operand_idx = 2) { | |
| auto* val = Value(inst.GetSingleWordOperand(first_operand_idx)); | |
| auto* unary = b_.Unary(op, val); | |
| if (auto* unary_inst = unary->AsInstruction()) { | |
| Emit(unary_inst, inst.result_id()); | |
| } else { | |
| AddValue(inst.result_id(), unary); | |
| } | |
| } | |
| /// @param inst the SPIR-V instruction | |
| /// @param op the binary operator to use | |
| void EmitBinary(const spvtools::opt::Instruction& inst, | |
| core::BinaryOp op, | |
| uint32_t first_operand_idx = 2) { | |
| auto* lhs = Value(inst.GetSingleWordOperand(first_operand_idx)); | |
| auto* rhs = Value(inst.GetSingleWordOperand(first_operand_idx + 1)); | |
| auto* binary = b_.Binary(op, Type(inst.type_id()), lhs, rhs); | |
| if (auto* res = binary->AsInstruction()) { | |
| Emit(res, inst.result_id()); | |
| } else { | |
| AddValue(inst.result_id(), binary); | |
| } | |
| } | |
| /// Emits the logical negation of the result of the given SPIR-V instruction. | |
| /// @param inst the SPIR-V instruction | |
| /// @param op the binary operator to use | |
| void EmitInvertedBinary(const spvtools::opt::Instruction& inst, core::BinaryOp op) { | |
| auto* lhs = Value(inst.GetSingleWordOperand(2)); | |
| auto* rhs = Value(inst.GetSingleWordOperand(3)); | |
| auto* binary = b_.Binary(op, Type(inst.type_id()), lhs, rhs); | |
| EmitWithoutSpvResult(binary); | |
| auto* inv = b_.Not(binary); | |
| if (auto* inv_inst = inv->AsInstruction()) { | |
| Emit(inv_inst, inst.result_id()); | |
| } else { | |
| AddValue(inst.result_id(), inv); | |
| } | |
| } | |
| /// @param inst the SPIR-V instruction for OpCompositeExtract | |
| void EmitCompositeExtract(const spvtools::opt::Instruction& inst, | |
| uint32_t composite_index = 2) { | |
| Vector<core::ir::Value*, 4> indices; | |
| for (uint32_t i = composite_index + 1; i < inst.NumOperandWords(); i++) { | |
| indices.Push(b_.Constant(u32(inst.GetSingleWordOperand(i)))); | |
| } | |
| auto* object = Value(inst.GetSingleWordOperand(composite_index)); | |
| auto* access = b_.Access(Type(inst.type_id()), object, std::move(indices)); | |
| EmitOrAdd(access, inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction for OpCompositeInsert | |
| void EmitCompositeInsert(const spvtools::opt::Instruction& inst) { | |
| auto* object = Value(inst.GetSingleWordOperand(2)); | |
| auto* composite = Value(inst.GetSingleWordOperand(3)); | |
| Vector<core::ir::Value*, 4> indices; | |
| for (uint32_t i = 4; i < inst.NumOperandWords(); i++) { | |
| indices.Push(b_.Constant(u32(inst.GetSingleWordOperand(i)))); | |
| } | |
| auto* tmp = b_.Var(ty_.ptr(function, Type(inst.type_id()))); | |
| tmp->SetInitializer(composite); | |
| auto* ptr_ty = ty_.ptr(function, object->Type()); | |
| auto* access = b_.Access(ptr_ty, tmp, std::move(indices)); | |
| EmitWithoutSpvResult(tmp->Result()); | |
| EmitWithoutSpvResult(access); | |
| EmitWithoutResult(b_.Store(access, object)); | |
| Emit(b_.Load(tmp), inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction for OpCompositeConstruct | |
| void EmitConstruct(const spvtools::opt::Instruction& inst) { | |
| auto* construct = b_.Construct(Type(inst.type_id()), Args(inst, 2)); | |
| EmitOrAdd(construct, inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction for OpVectorInsertDynamic | |
| void EmitVectorInsertDynamic(const spvtools::opt::Instruction& inst) { | |
| auto vector = Value(inst.GetSingleWordOperand(2)); | |
| auto component = Value(inst.GetSingleWordOperand(3)); | |
| auto index = Value(inst.GetSingleWordOperand(4)); | |
| auto* tmp = b_.Var( | |
| ty_.ptr(core::AddressSpace::kFunction, Type(inst.type_id()), core::Access::kReadWrite)); | |
| tmp->SetInitializer(vector); | |
| EmitWithoutSpvResult(tmp->Result()); | |
| EmitWithoutResult(b_.StoreVectorElement(tmp, index, component)); | |
| Emit(b_.Load(tmp), inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction for OpVectorShuffle | |
| void EmitVectorShuffle(const spvtools::opt::Instruction& inst) { | |
| auto* vector1 = Value(inst.GetSingleWordOperand(2)); | |
| auto* vector2 = Value(inst.GetSingleWordOperand(3)); | |
| auto* result_ty = Type(inst.type_id()); | |
| auto* el_ty = result_ty->DeepestElement(); | |
| uint32_t n1 = vector1->Type()->As<core::type::Vector>()->Width(); | |
| uint32_t n2 = vector2->Type()->As<core::type::Vector>()->Width(); | |
| Vector<core::ir::Value*, 4> swizzles; | |
| // Track the current swizzle that we are building from consecutive indices that fall within | |
| // the same vector. | |
| Vector<uint32_t, 4> current_indices; | |
| core::ir::Value* current_vector = nullptr; | |
| // Emit the current swizzle that we have constructed so far, and add it to the list. | |
| auto flush_swizzle = [&] { | |
| if (current_vector == nullptr) { | |
| return; | |
| } | |
| auto* swizzle_type = ty_.MatchWidth(el_ty, current_indices.Length()); | |
| auto* swizzle = b_.Swizzle(swizzle_type, current_vector, current_indices); | |
| EmitWithoutSpvResult(swizzle); | |
| swizzles.Push(swizzle); | |
| current_indices.Clear(); | |
| }; | |
| for (uint32_t i = 4; i < inst.NumOperandWords(); i++) { | |
| uint32_t literal = inst.GetSingleWordOperand(i); | |
| // Determine which vector this index falls within. | |
| uint32_t next_index; | |
| core::ir::Value* next_vector = nullptr; | |
| if (literal == 0xFFFFFFFF) { | |
| // Undefined component, so just use the first component of the first vector. | |
| next_vector = vector1; | |
| next_index = 0; | |
| } else if (literal < n1) { | |
| next_vector = vector1; | |
| next_index = literal; | |
| } else if (literal < n1 + n2) { | |
| next_vector = vector2; | |
| next_index = literal - n1; | |
| } else { | |
| TINT_ICE() << "invalid vector shuffle index"; | |
| } | |
| // If the vector has changed from the previous index, flush the swizzle. | |
| if (next_vector != current_vector) { | |
| flush_swizzle(); | |
| } | |
| current_vector = next_vector; | |
| current_indices.Push(next_index); | |
| } | |
| flush_swizzle(); | |
| if (swizzles.Length() == 1) { | |
| // There was only one swizzle, so we can just use it directly. | |
| AddValue(inst.result_id(), swizzles[0]); | |
| } else { | |
| // There were multiple swizzles, so we combine them all to produce the final result. | |
| EmitOrAdd(b_.Construct(result_ty, swizzles), inst.result_id()); | |
| } | |
| } | |
| /// @param inst the SPIR-V instruction for OpFunctionCall | |
| void EmitFunctionCall(const spvtools::opt::Instruction& inst) { | |
| Emit(b_.Call(Function(inst.GetSingleWordInOperand(0)), Args(inst, 3)), inst.result_id()); | |
| } | |
| /// @param inst the SPIR-V instruction for OpVariable | |
| Result<SuccessType> EmitVar(const spvtools::opt::Instruction& inst) { | |
| // Handle decorations. | |
| std::optional<uint32_t> group; | |
| std::optional<uint32_t> binding; | |
| core::Access access_mode = core::Access::kUndefined; | |
| core::IOAttributes io_attributes; | |
| auto interpolation = [&]() -> core::Interpolation& { | |
| // Create the interpolation field with the default values on first call. | |
| if (!io_attributes.interpolation.has_value()) { | |
| io_attributes.interpolation = core::Interpolation{ | |
| .type = core::InterpolationType::kPerspective, | |
| .sampling = core::InterpolationSampling::kUndefined, | |
| }; | |
| } | |
| return io_attributes.interpolation.value(); | |
| }; | |
| for (auto* deco : | |
| spirv_context_->get_decoration_mgr()->GetDecorationsFor(inst.result_id(), false)) { | |
| auto d = deco->GetSingleWordOperand(1); | |
| switch (spv::Decoration(d)) { | |
| case spv::Decoration::RelaxedPrecision: // WGSL is relaxed precision by default | |
| break; | |
| case spv::Decoration::NonReadable: | |
| access_mode = core::Access::kWrite; | |
| break; | |
| case spv::Decoration::NonWritable: | |
| access_mode = core::Access::kRead; | |
| break; | |
| case spv::Decoration::DescriptorSet: | |
| group = deco->GetSingleWordOperand(2); | |
| break; | |
| case spv::Decoration::Binding: | |
| binding = deco->GetSingleWordOperand(2); | |
| break; | |
| case spv::Decoration::BuiltIn: { | |
| TINT_CHECK_RESULT_UNWRAP(builtin_val, | |
| Builtin(spv::BuiltIn(deco->GetSingleWordOperand(2)))); | |
| io_attributes.builtin = builtin_val; | |
| break; | |
| } | |
| case spv::Decoration::Invariant: | |
| io_attributes.invariant = true; | |
| break; | |
| case spv::Decoration::Location: | |
| io_attributes.location = deco->GetSingleWordOperand(2); | |
| break; | |
| case spv::Decoration::NoPerspective: | |
| interpolation().type = core::InterpolationType::kLinear; | |
| break; | |
| case spv::Decoration::Flat: | |
| interpolation().type = core::InterpolationType::kFlat; | |
| break; | |
| case spv::Decoration::Centroid: | |
| interpolation().sampling = core::InterpolationSampling::kCentroid; | |
| break; | |
| case spv::Decoration::Sample: | |
| interpolation().sampling = core::InterpolationSampling::kSample; | |
| break; | |
| case spv::Decoration::Index: | |
| io_attributes.blend_src = deco->GetSingleWordOperand(2); | |
| break; | |
| case spv::Decoration::Coherent: | |
| // Tint has coherent memory semantics, so this is a no-op. | |
| break; | |
| case spv::Decoration::Restrict: | |
| // Hint to the compiler that it may compile as if there is no aliasing. Ignore. | |
| break; | |
| default: | |
| return Failure("unhandled decoration " + | |
| std::string(spv::DecorationToString(spv::Decoration(d)))); | |
| } | |
| } | |
| if (io_attributes.interpolation.has_value()) { | |
| // WGSL requires that '@interpolate(flat)' needs to be paired with '@location', however | |
| // SPIR-V requires all fragment shader integer Inputs are 'flat'. If the decorations do | |
| // not contain a spv::Decoration::Location, then remove the interpolation decoration. | |
| // | |
| // The `perspective,center` interpolation is the default value if one isn't provided. | |
| // Just strip it off. This keeps us from accidentally applying interpolation where it | |
| // isn't permitted, and it isn't necessary. | |
| if ((io_attributes.interpolation->type == core::InterpolationType::kFlat && | |
| !io_attributes.location.has_value()) || | |
| (io_attributes.interpolation->type == core::InterpolationType::kPerspective && | |
| io_attributes.interpolation->sampling == core::InterpolationSampling::kCenter)) { | |
| io_attributes.interpolation = std::nullopt; | |
| } | |
| } | |
| auto* element_ty = Type(inst.type_id(), access_mode)->As<core::type::Pointer>(); | |
| if (element_ty->AddressSpace() == core::AddressSpace::kOut) { | |
| if (element_ty->StoreType()->IsIntegerScalarOrVector()) { | |
| io_attributes.interpolation = { | |
| .type = core::InterpolationType::kFlat, | |
| .sampling = core::InterpolationSampling::kUndefined, | |
| }; | |
| } else if (auto* str = element_ty->StoreType()->As<core::type::Struct>()) { | |
| for (auto* member : str->Members()) { | |
| if (member->Type()->IsIntegerScalarOrVector()) { | |
| const_cast<core::type::StructMember*>(member)->SetInterpolation( | |
| core::Interpolation{core::InterpolationType::kFlat, | |
| core::InterpolationSampling::kUndefined}); | |
| } | |
| } | |
| } | |
| } | |
| auto* var = b_.Var(element_ty); | |
| if (inst.NumOperands() > 3) { | |
| var->SetInitializer(Value(inst.GetSingleWordOperand(3))); | |
| } | |
| if (group || binding) { | |
| TINT_ASSERT(group && binding); | |
| // Remap any samplers which match an entry in the sampler mappings | |
| // table. | |
| if (element_ty->StoreType()->Is<core::type::Sampler>()) { | |
| auto it = | |
| options_.sampler_mappings.find(BindingPoint{group.value(), binding.value()}); | |
| if (it != options_.sampler_mappings.end()) { | |
| auto bp = it->second; | |
| group = bp.group; | |
| binding = bp.binding; | |
| } | |
| } | |
| auto& grp = max_binding.GetOrAddZero(group.value()); | |
| grp = std::max(grp, binding.value()); | |
| auto& used = used_bindings.GetOrAddZero(BindingPoint{group.value(), binding.value()}); | |
| used += 1; | |
| io_attributes.binding_point = {group.value(), binding.value()}; | |
| } | |
| var->SetAttributes(std::move(io_attributes)); | |
| var_to_original_access_mode_.insert({var, access_mode}); | |
| Emit(var, inst.result_id()); | |
| return Success; | |
| } | |
| private: | |
| /// TypeKey describes a SPIR-V type with an access mode. | |
| struct TypeKey { | |
| /// The SPIR-V type object. | |
| uint32_t type_id; | |
| /// The access mode. | |
| core::Access access_mode; | |
| // Equality operator for TypeKey. | |
| bool operator==(const TypeKey& other) const { | |
| return type_id == other.type_id && access_mode == other.access_mode; | |
| } | |
| /// @returns the hash code of the TypeKey | |
| tint::HashCode HashCode() const { return Hash(type_id, access_mode); } | |
| }; | |
| /// The parser options | |
| const Options& options_; | |
| /// The generated IR module. | |
| core::ir::Module ir_; | |
| /// The Tint IR builder. | |
| core::ir::Builder b_{ir_}; | |
| /// The Tint type manager. | |
| core::type::Manager& ty_{ir_.Types()}; | |
| /// The Tint IR function that is currently being emitted. | |
| core::ir::Function* current_function_ = nullptr; | |
| /// The Tint IR block that is currently being emitted. | |
| core::ir::Block* current_block_ = nullptr; | |
| /// A map from a SPIR-V type declaration to the corresponding Tint type object. | |
| Hashmap<TypeKey, const core::type::Type*, 16> types_; | |
| /// A map from a SPIR-V function definition result ID to the corresponding Tint function object. | |
| Hashmap<uint32_t, core::ir::Function*, 8> functions_; | |
| /// A map from a SPIR-V result ID to the corresponding Tint value object. | |
| Hashmap<uint32_t, core::ir::Value*, 8> values_; | |
| /// Maps a `group` number to the largest seen `binding` value for that group | |
| Hashmap<uint32_t, uint32_t, 4> max_binding; | |
| /// A map of binding point to the count of usages | |
| Hashmap<BindingPoint, uint32_t, 4> used_bindings; | |
| /// The SPIR-V context containing the SPIR-V tools intermediate representation. | |
| std::unique_ptr<spvtools::opt::IRContext> spirv_context_; | |
| /// The current SPIR-V function being emitted | |
| spvtools::opt::Function* current_spirv_function_ = nullptr; | |
| // The set of IDs that are imports of the GLSL.std.450 extended instruction sets. | |
| std::unordered_set<uint32_t> glsl_std_450_imports_; | |
| // The set of IDs of imports that are ignored. For example, any "NonSemanticInfo." import is | |
| // ignored. | |
| std::unordered_set<uint32_t> ignored_imports_; | |
| // Map of SPIR-V IDs to string names | |
| std::unordered_map<uint32_t, std::string> id_to_name_; | |
| // Map of SPIR-V Struct IDs to a list of member string names | |
| std::unordered_map<uint32_t, std::vector<std::string>> struct_to_member_names_; | |
| // Set of types which should be considered `NonWritable` even if no decoration is present | |
| std::unordered_set<const core::type::Type*> consider_non_writable_; | |
| // Set of SPIR-V block ids where we'll stop a `Branch` instruction walk. These could be merge | |
| // blocks, premerge blocks, continuing blocks, etc. | |
| std::unordered_map<uint32_t, core::ir::ControlInstruction*> walk_stop_blocks_; | |
| // Map of continue target ID to the controlling IR loop. | |
| std::unordered_map<uint32_t, core::ir::Loop*> continue_targets_; | |
| // Map of header target ID to the controlling IR loop. | |
| std::unordered_map<uint32_t, core::ir::Loop*> loop_headers_; | |
| struct PremergeInfo { | |
| core::ir::If* parent = nullptr; | |
| core::ir::Var* condition = nullptr; | |
| }; | |
| // Map of merge ID to an associated premerge_id, if any | |
| std::unordered_map<uint32_t, PremergeInfo> merge_to_premerge_; | |
| std::unordered_set<core::ir::Block*> current_blocks_; | |
| // For each block, we keep a set of SPIR-V `id`s which are known in that scope. | |
| std::vector<std::unordered_set<uint32_t>> id_stack_; | |
| // If we're in a switch, is populated with the IDs of the blocks for each of the switch | |
| // selectors. This lets us watch for fallthrough when emitting branch instructions. | |
| std::vector<std::unordered_set<uint32_t>> current_switch_blocks_; | |
| /// Maps from a spirv-v block id to the corresponding block in the IR | |
| std::unordered_map<uint32_t, core::ir::Block*> spirv_id_to_block_; | |
| // Map of continue block id to the phi types which need to be returned by | |
| // the continue target | |
| std::unordered_map<uint32_t, std::vector<uint32_t>> continue_blk_phis_; | |
| // A stack of values which need to be replaced as we finish processing a | |
| // block. Used to store `phi` information so we can retrieve values which | |
| // are defined after the `OpPhi` instruction. | |
| std::vector<std::vector<ReplacementValue>> values_to_replace_; | |
| // A map of block to phi values returned by the loop header | |
| std::unordered_map<uint32_t, std::vector<uint32_t>> block_phi_values_; | |
| // Map of certain instructions back to their originating spirv block | |
| std::unordered_map<core::ir::Instruction*, uint32_t> inst_to_spirv_block_; | |
| // Map of merge block id's back to the block which contains the header | |
| std::unordered_map<uint32_t, uint32_t> spirv_merge_id_to_header_id_; | |
| // Structure hold spec composite information | |
| struct SpecComposite { | |
| // The composite type | |
| const core::type::Type* type = nullptr; | |
| // The composite arguments | |
| Vector<uint32_t, 4> args; | |
| }; | |
| // The set of SPIR-V IDs which map to `OpSpecConstantComposite` information | |
| std::unordered_map<uint32_t, SpecComposite> spec_composites_; | |
| // Structures which are marked with `BufferBlock` and need to be reported as `Storage` address | |
| // space | |
| std::unordered_set<const core::type::Struct*> storage_buffer_types_; | |
| // Map of `var` to the access mode it was originally created with. This may be different from | |
| // the current mode if we needed to set a default mode. | |
| std::unordered_map<core::ir::Var*, core::Access> var_to_original_access_mode_; | |
| // Map of spir-v branch conditional instructions to the related IR if instruction. | |
| std::unordered_map<const spvtools::opt::Instruction*, core::ir::If*> branch_conditional_to_if_; | |
| }; | |
| } // namespace | |
| Result<core::ir::Module> Parse(std::span<const uint32_t> spirv, const Options& options) { | |
| return Parser(options).Run(spirv); | |
| } | |
| } // namespace tint::spirv::reader |