| // Copyright 2025 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| #include "src/tint/lang/spirv/reader/lower/texture.h" | |
| #include <tuple> | |
| #include <utility> | |
| #include <vector> | |
| #include "src/tint/lang/core/ir/builder.h" | |
| #include "src/tint/lang/core/ir/clone_context.h" | |
| #include "src/tint/lang/core/ir/module.h" | |
| #include "src/tint/lang/core/ir/validator/validate.h" | |
| #include "src/tint/lang/core/type/sampled_texture.h" | |
| #include "src/tint/lang/core/type/storage_texture.h" | |
| #include "src/tint/lang/spirv/builtin_fn.h" | |
| #include "src/tint/lang/spirv/ir/builtin_call.h" | |
| #include "src/tint/lang/spirv/type/image.h" | |
| namespace tint::spirv::reader::lower { | |
| namespace { | |
| using namespace tint::core::fluent_types; // NOLINT | |
| using namespace tint::core::number_suffixes; // NOLINT | |
| enum class ImageOperandsMask : uint32_t { | |
| kBias = 0x00000001, | |
| kLod = 0x00000002, | |
| kGrad = 0x00000004, | |
| kConstOffset = 0x00000008, | |
| kSample = 0x00000040, | |
| }; | |
| /// PIMPL state for the transform. | |
| struct State { | |
| /// The IR module. | |
| core::ir::Module& ir; | |
| /// The IR builder. | |
| core::ir::Builder b{ir}; | |
| /// The type manager. | |
| core::type::Manager& ty{ir.Types()}; | |
| /// Map of all OpSampledImages seen | |
| Hashmap<core::ir::Value*, core::ir::Instruction*, 4> sampled_images_{}; | |
| /// Any `ir::UserCall` instructions which have texture params which need to be updated. | |
| Hashset<core::ir::UserCall*, 2> user_calls_to_convert_{}; | |
| /// The `ir::Values`s which have had their types changed, they then need to have their | |
| /// usages updated to match. This maps to the root FunctionParam or Var for each texture. | |
| Vector<core::ir::Value*, 8> values_to_fix_usages_{}; | |
| Vector<core::ir::Let*, 8> lets_to_inline_{}; | |
| /// Function to texture replacements | |
| Hashmap<core::ir::Function*, core::ir::Function*, 4> func_to_rewritten_{}; | |
| /// Textures used in dref calls which need to be depth textures. | |
| Vector<core::ir::Value*, 4> textures_to_convert_to_depth_{}; | |
| /// Samplers used in dref calls which need to be comparison samplers | |
| Vector<core::ir::Value*, 4> samplers_to_convert_to_comparison_{}; | |
| /// Process the module. | |
| void Process() { | |
| ReplacePointerToHandle(); | |
| 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* type = ptr->UnwrapPtr(); | |
| if (!type->IsAnyOf<spirv::type::Image, core::type::Sampler>()) { | |
| continue; | |
| } | |
| auto* new_ty = TypeFor(type); | |
| if (type->Is<spirv::type::Image>()) { | |
| var->Result()->SetType(ty.ptr(ptr->AddressSpace(), new_ty, ptr->Access())); | |
| values_to_fix_usages_.Push(var->Result()); | |
| } | |
| var->Result()->ForEachUseUnsorted([&](const core::ir::Usage& usage) { | |
| tint::Switch( | |
| usage.instruction, // | |
| [&](core::ir::Load* l) { | |
| if (type->Is<spirv::type::Image>()) { | |
| l->Result()->SetType(new_ty); | |
| } | |
| }, | |
| [&](core::ir::Let* let) { | |
| if (type->Is<core::type::Sampler>()) { | |
| lets_to_inline_.Push(let); | |
| } | |
| }); | |
| }); | |
| } | |
| std::vector<std::function<void()>> worklist; | |
| worklist.reserve(128); | |
| for (auto* inst : ir.Instructions()) { | |
| if (auto* builtin = inst->As<spirv::ir::BuiltinCall>()) { | |
| switch (builtin->Func()) { | |
| case spirv::BuiltinFn::kOpSampledImage: | |
| SampledImage(builtin); | |
| break; | |
| case spirv::BuiltinFn::kImageDrefGather: | |
| worklist.push_back( | |
| [this, builtin] { FindVarsForImageGatherDref(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageSampleDrefImplicitLod: | |
| case spirv::BuiltinFn::kImageSampleDrefExplicitLod: | |
| case spirv::BuiltinFn::kImageSampleProjDrefImplicitLod: | |
| case spirv::BuiltinFn::kImageSampleProjDrefExplicitLod: | |
| worklist.push_back( | |
| [this, builtin] { FindVarsForImageSampleDref(builtin); }); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| // For each depth function, find the parameters which needed be converted. We don't convert | |
| // the actual calls yet, as we may need to parameters to have propagated through function | |
| // parameters. | |
| for (auto& cb : worklist) { | |
| cb(); | |
| } | |
| worklist.clear(); | |
| Hashset<core::ir::Value*, 4> converted{}; | |
| while (!textures_to_convert_to_depth_.IsEmpty()) { | |
| auto tex = textures_to_convert_to_depth_.Pop(); | |
| if (!converted.Add(tex)) { | |
| continue; | |
| } | |
| tint::Switch( | |
| FindRootVarFor(tex), // | |
| [&](core::ir::InstructionResult* res) { | |
| auto* var = res->Instruction()->As<core::ir::Var>(); | |
| TINT_ASSERT(var); | |
| ConvertVarToDepth(var); | |
| }, | |
| [&](core::ir::FunctionParam* param) { ConvertTextureParam(param); }, // | |
| TINT_ICE_ON_NO_MATCH); | |
| } | |
| while (!samplers_to_convert_to_comparison_.IsEmpty()) { | |
| auto samp = samplers_to_convert_to_comparison_.Pop(); | |
| if (!converted.Add(samp)) { | |
| continue; | |
| } | |
| tint::Switch( | |
| FindRootVarFor(samp), // | |
| [&](core::ir::InstructionResult* res) { | |
| auto* var = res->Instruction()->As<core::ir::Var>(); | |
| TINT_ASSERT(var); | |
| ConvertVarToComparison(var); | |
| }, | |
| [&](core::ir::FunctionParam* param) { ConvertSamplerParam(param); }, // | |
| TINT_ICE_ON_NO_MATCH); | |
| } | |
| UpdateValues(); | |
| Vector<spirv::ir::BuiltinCall*, 4> builtin_worklist; | |
| for (auto* inst : ir.Instructions()) { | |
| if (auto* builtin = inst->As<spirv::ir::BuiltinCall>()) { | |
| switch (builtin->Func()) { | |
| case spirv::BuiltinFn::kOpSampledImage: | |
| // Note, we _also_ do this here even though it was done above. The one above | |
| // registers for the depth functions, but, we may have forked functions in | |
| // the `UpdateValues` when it does the `ConvertUserCall`. This would then | |
| // generate new `SampledImage` objects which need to be registered. In the | |
| // worse case, we just write the same data twice. | |
| SampledImage(builtin); | |
| break; | |
| case spirv::BuiltinFn::kOpImage: | |
| worklist.push_back([this, builtin] { Image(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageRead: | |
| case spirv::BuiltinFn::kImageFetch: | |
| worklist.push_back([this, builtin] { ImageFetch(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageGather: | |
| worklist.push_back([this, builtin] { ImageGather(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageQueryLevels: | |
| worklist.push_back([this, builtin] { | |
| ImageQuery(builtin, core::BuiltinFn::kTextureNumLevels); | |
| }); | |
| break; | |
| case spirv::BuiltinFn::kImageQuerySamples: | |
| worklist.push_back([this, builtin] { | |
| ImageQuery(builtin, core::BuiltinFn::kTextureNumSamples); | |
| }); | |
| break; | |
| case spirv::BuiltinFn::kImageQuerySize: | |
| case spirv::BuiltinFn::kImageQuerySizeLod: | |
| worklist.push_back([this, builtin] { ImageQuerySize(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageSampleExplicitLod: | |
| case spirv::BuiltinFn::kImageSampleImplicitLod: | |
| case spirv::BuiltinFn::kImageSampleProjImplicitLod: | |
| case spirv::BuiltinFn::kImageSampleProjExplicitLod: | |
| worklist.push_back([this, builtin] { ImageSample(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageWrite: | |
| worklist.push_back([this, builtin] { ImageWrite(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageSampleDrefImplicitLod: | |
| case spirv::BuiltinFn::kImageSampleDrefExplicitLod: | |
| case spirv::BuiltinFn::kImageSampleProjDrefImplicitLod: | |
| case spirv::BuiltinFn::kImageSampleProjDrefExplicitLod: | |
| worklist.push_back([this, builtin] { ImageSampleDref(builtin); }); | |
| break; | |
| case spirv::BuiltinFn::kImageDrefGather: | |
| worklist.push_back([this, builtin] { ImageGatherDref(builtin); }); | |
| break; | |
| default: | |
| TINT_UNREACHABLE() << "unknown spirv builtin: " << builtin->Func(); | |
| } | |
| } | |
| } | |
| for (auto& cb : worklist) { | |
| cb(); | |
| } | |
| // Destroy all the OpSampledImage instructions. | |
| for (auto res : sampled_images_) { | |
| // If the sampled image was in a user function which was forked and the original | |
| // destroyed then it will no longer be alive. | |
| if (res.value->Alive()) { | |
| res.value->Destroy(); | |
| } | |
| } | |
| } | |
| void ReplacePointerToHandle() { | |
| Vector<core::ir::Value*, 4> usages_to_update; | |
| Hashset<core::ir::Function*, 4> called_functions_to_fixup; | |
| for (auto& func : ir.DependencyOrderedFunctions()) { | |
| for (auto* param : func->Params()) { | |
| auto* ptr = param->Type()->As<core::type::Pointer>(); | |
| if (!ptr || !ptr->StoreType()->IsHandle()) { | |
| continue; | |
| } | |
| param->SetType(ptr->StoreType()); | |
| usages_to_update.Push(param); | |
| called_functions_to_fixup.Add(func); | |
| } | |
| } | |
| Vector<core::ir::Instruction*, 4> inst_to_delete; | |
| while (!usages_to_update.IsEmpty()) { | |
| auto* val = usages_to_update.Pop(); | |
| for (auto& usage : val->UsagesSorted()) { | |
| tint::Switch( | |
| usage.instruction, // | |
| [&](core::ir::Load* ld) { | |
| ld->Result()->ReplaceAllUsesWith(ld->From()); | |
| inst_to_delete.Push(ld); | |
| }, | |
| [&](core::ir::Let* l) { | |
| for (auto& u : l->Result()->UsagesSorted()) { | |
| usages_to_update.Push(u.instruction->Result()); | |
| } | |
| l->Result()->ReplaceAllUsesWith(l->Value()); | |
| inst_to_delete.Push(l); | |
| }); | |
| } | |
| } | |
| for (auto& fn : called_functions_to_fixup) { | |
| for (auto& usage : fn->UsagesUnsorted()) { | |
| auto* call = usage->instruction->As<core::ir::UserCall>(); | |
| if (!call) { | |
| continue; | |
| } | |
| auto args = call->Args(); | |
| for (size_t i = 0; i < args.size(); ++i) { | |
| auto& arg = args[i]; | |
| auto* ptr_ty = arg->Type()->As<core::type::Pointer>(); | |
| if (!ptr_ty) { | |
| continue; | |
| } | |
| if (!ptr_ty->StoreType()->IsHandle()) { | |
| continue; | |
| } | |
| // We inject the load here but it will get cleaned up when we deal with the | |
| // function itself, if required. | |
| b.InsertBefore(usage->instruction, [&] { | |
| auto* ld = b.Load(arg); | |
| call->SetArg(i, ld->Result()); | |
| }); | |
| } | |
| } | |
| } | |
| while (!inst_to_delete.IsEmpty()) { | |
| auto* inst = inst_to_delete.Pop(); | |
| inst->Destroy(); | |
| } | |
| } | |
| void UpdateValues() { | |
| // The double loop happens because when we convert user calls, that will | |
| // add more values to convert, but those values can find user calls to | |
| // convert, so we have to work until we stabilize | |
| while (!values_to_fix_usages_.IsEmpty() || !lets_to_inline_.IsEmpty() || | |
| !user_calls_to_convert_.IsEmpty()) { | |
| while (!values_to_fix_usages_.IsEmpty()) { | |
| auto* val = values_to_fix_usages_.Pop(); | |
| ConvertUsagesToTexture(val); | |
| } | |
| while (!lets_to_inline_.IsEmpty()) { | |
| auto* let = lets_to_inline_.Pop(); | |
| TINT_ASSERT(let->Value()); | |
| let->Result()->ReplaceAllUsesWith(let->Value()); | |
| // We may have done this value already, but push it back on as any of the let usages | |
| // will now point to it and they need to be updated. | |
| values_to_fix_usages_.Push(let->Value()); | |
| let->Destroy(); | |
| } | |
| auto user_calls = user_calls_to_convert_.Vector(); | |
| // Sort for deterministic output | |
| user_calls.Sort(); | |
| for (auto& call : user_calls) { | |
| ConvertUserCall(call); | |
| } | |
| user_calls_to_convert_.Clear(); | |
| } | |
| TINT_ASSERT(lets_to_inline_.IsEmpty()); | |
| TINT_ASSERT(values_to_fix_usages_.IsEmpty()); | |
| TINT_ASSERT(user_calls_to_convert_.IsEmpty()); | |
| } | |
| // Given a value, walk back up and find the root `var`. | |
| core::ir::Value* FindRootVarFor(core::ir::Value* val) { | |
| if (val->Is<core::ir::FunctionParam>()) { | |
| return val; | |
| } | |
| auto* inst_res = val->As<core::ir::InstructionResult>(); | |
| TINT_ASSERT(inst_res); | |
| return tint::Switch( | |
| inst_res->Instruction(), // | |
| [&](core::ir::Let* l) { return FindRootVarFor(l->Value()); }, | |
| [&](core::ir::Load* l) { return FindRootVarFor(l->From()); }, | |
| [&](core::ir::Var* v) { return v->Result(); }, // | |
| TINT_ICE_ON_NO_MATCH); | |
| } | |
| // Given a function parameter to convert to either a depth texture or comparison sampler we get | |
| // all the call sites for the function. We then mark the argument value for the given parameter | |
| // for conversion. This will then trigger that variable to convert up the call stack. Eventually | |
| // we'll stop and convert the root variable. Then, that conversion will trigger a walk back down | |
| // which will convert the call argument types and do any forking of functions to handle the new | |
| // parameter types. | |
| void ConvertTextureParam(core::ir::FunctionParam* param) { | |
| for (auto& usage : param->Function()->UsagesUnsorted()) { | |
| auto* caller = usage->instruction->As<core::ir::UserCall>(); | |
| if (!caller) { | |
| continue; | |
| } | |
| textures_to_convert_to_depth_.Push(caller->Args()[param->Index()]); | |
| } | |
| } | |
| void ConvertSamplerParam(core::ir::FunctionParam* param) { | |
| for (auto& usage : param->Function()->UsagesUnsorted()) { | |
| auto* caller = usage->instruction->As<core::ir::UserCall>(); | |
| if (!caller) { | |
| continue; | |
| } | |
| samplers_to_convert_to_comparison_.Push(caller->Args()[param->Index()]); | |
| } | |
| } | |
| void ConvertVarToDepth(core::ir::Var* var) { | |
| auto* orig_ptr_ty = var->Result()->Type()->As<core::type::Pointer>(); | |
| TINT_ASSERT(orig_ptr_ty); | |
| auto* orig_tex_ty = orig_ptr_ty->UnwrapPtr()->As<core::type::Texture>(); | |
| TINT_ASSERT(orig_tex_ty); | |
| const core::type::Type* depth_ty = nullptr; | |
| if (orig_tex_ty->Is<core::type::MultisampledTexture>()) { | |
| depth_ty = ty.depth_multisampled_texture(orig_tex_ty->Dim()); | |
| } else { | |
| depth_ty = ty.depth_texture(orig_tex_ty->Dim()); | |
| } | |
| auto* depth_ptr = ty.ptr(orig_ptr_ty->AddressSpace(), depth_ty, orig_ptr_ty->Access()); | |
| var->Result()->SetType(depth_ptr); | |
| values_to_fix_usages_.Push(var->Result()); | |
| } | |
| void ConvertVarToComparison(core::ir::Var* var) { | |
| auto* orig_ptr_ty = var->Result()->Type()->As<core::type::Pointer>(); | |
| TINT_ASSERT(orig_ptr_ty); | |
| auto* sampler = ty.comparison_sampler(); | |
| auto* sampler_ptr = ty.ptr(orig_ptr_ty->AddressSpace(), sampler, orig_ptr_ty->Access()); | |
| var->Result()->SetType(sampler_ptr); | |
| values_to_fix_usages_.Push(var->Result()); | |
| } | |
| // Stores information for operands which need to be updated with the new load result. | |
| struct ReplacementValue { | |
| // The instruction to update | |
| core::ir::Instruction* instruction; | |
| // The operand index to insert into | |
| size_t idx; | |
| // The new value to insert into the instruction operands at `idx` | |
| core::ir::Value* value; | |
| }; | |
| void ConvertUsagesToTexture(core::ir::Value* val) { | |
| val->ForEachUseUnsorted([&](const core::ir::Usage& usage) { | |
| auto* inst = usage.instruction; | |
| tint::Switch( // | |
| inst, // | |
| [&](core::ir::Let* l) { lets_to_inline_.Push(l); }, | |
| [&](core::ir::Load* l) { | |
| auto* res = l->Result(); | |
| res->SetType(val->Type()->UnwrapPtr()); | |
| values_to_fix_usages_.Push(res); | |
| }, // | |
| [&](core::ir::UserCall* uc) { user_calls_to_convert_.Add(uc); }, | |
| [&](core::ir::BuiltinCall*) {}, // | |
| TINT_ICE_ON_NO_MATCH); | |
| }); | |
| } | |
| // The user calls need to check all of the parameters which were converted | |
| // to textures and create a forked function call for that combination of | |
| // parameters. | |
| void ConvertUserCall(core::ir::UserCall* uc) { | |
| auto* target = uc->Target(); | |
| auto& params = target->Params(); | |
| const auto& args = uc->Args(); | |
| Vector<size_t, 2> to_convert; | |
| for (size_t i = 0; i < args.size(); ++i) { | |
| if (params[i]->Type() != args[i]->Type()) { | |
| to_convert.Push(i); | |
| } | |
| } | |
| // Everything is already converted we're done. | |
| if (to_convert.IsEmpty()) { | |
| return; | |
| } | |
| auto* new_fn = func_to_rewritten_.GetOrAdd(target, [&] { | |
| core::ir::CloneContext ctx{ir}; | |
| auto* fn = uc->Target()->Clone(ctx); | |
| ir.functions.Push(fn); | |
| for (auto idx : to_convert) { | |
| auto* p = fn->Params()[idx]; | |
| p->SetType(args[idx]->Type()); | |
| values_to_fix_usages_.Push(p); | |
| } | |
| return fn; | |
| }); | |
| uc->SetTarget(new_fn); | |
| if (target->IsEntryPoint()) { | |
| return; | |
| } | |
| // Check if any of the usages are calls, if they aren't we can destroy the function. | |
| for (auto& usage : target->UsagesUnsorted()) { | |
| if (usage->instruction->Is<core::ir::Call>()) { | |
| return; | |
| } | |
| } | |
| ir.Destroy(target); | |
| } | |
| // Record the sampled image so we can extract the texture/sampler information as we process the | |
| // builtins. It will be destroyed after all builtins are done. | |
| void SampledImage(spirv::ir::BuiltinCall* call) { sampled_images_.Add(call->Result(), call); } | |
| std::pair<core::ir::Value*, core::ir::Value*> GetTextureSampler(core::ir::Value* sampled) { | |
| auto res = sampled_images_.Get(sampled); | |
| TINT_ASSERT(res); | |
| core::ir::Instruction* inst = *res; | |
| TINT_ASSERT(inst->Operands().Length() == 2); | |
| return {inst->Operands()[0], inst->Operands()[1]}; | |
| } | |
| uint32_t CoordsRequiredForDim(core::type::TextureDimension dim, bool is_proj) { | |
| uint32_t ret = 0; | |
| if (is_proj) { | |
| ++ret; | |
| } | |
| switch (dim) { | |
| case core::type::TextureDimension::k1d: | |
| ret += 1; | |
| break; | |
| case core::type::TextureDimension::k2d: | |
| ret += 2; | |
| break; | |
| case core::type::TextureDimension::k2dArray: | |
| case core::type::TextureDimension::k3d: | |
| case core::type::TextureDimension::kCube: | |
| ret += 3; | |
| break; | |
| case core::type::TextureDimension::kCubeArray: | |
| ret += 4; | |
| break; | |
| default: | |
| TINT_UNREACHABLE(); | |
| } | |
| return ret; | |
| } | |
| uint32_t Length(const core::type::Type* type) { | |
| if (type->IsScalar()) { | |
| return 1; | |
| } | |
| if (auto* vec = type->As<core::type::Vector>()) { | |
| return vec->Width(); | |
| } | |
| TINT_UNREACHABLE(); | |
| } | |
| void ProcessCoords(const core::type::Type* type, | |
| bool is_proj, | |
| core::ir::Value* coords, | |
| Vector<core::ir::Value*, 5>& new_args) { | |
| auto* tex_ty = type->As<core::type::Texture>(); | |
| TINT_ASSERT(tex_ty); | |
| auto coords_received = Length(coords->Type()); | |
| auto mk_coords = [&](uint32_t count) -> core::ir::Value* { | |
| if (count == coords_received) { | |
| return coords; | |
| } | |
| Vector<uint32_t, 3> swizzle_idx = {0}; | |
| for (uint32_t i = 1; i < count; ++i) { | |
| swizzle_idx.Push(i); | |
| } | |
| auto* new_ty = ty.MatchWidth(coords->Type()->DeepestElement(), count); | |
| return b.Swizzle(new_ty, coords, swizzle_idx); | |
| }; | |
| auto coords_needed = CoordsRequiredForDim(tex_ty->Dim(), is_proj); | |
| TINT_ASSERT(coords_needed <= coords_received); | |
| if (!is_proj && !IsTextureArray(tex_ty->Dim())) { | |
| new_args.Push(mk_coords(coords_needed)); | |
| return; | |
| } | |
| auto* coords_ty = coords->Type()->As<core::type::Vector>(); | |
| TINT_ASSERT(coords_ty); | |
| // The array / projection index, is on the end | |
| uint32_t new_coords_width = coords_needed - 1; | |
| auto* new_coords_ty = ty.MatchWidth(coords_ty->Type(), new_coords_width); | |
| auto* swizzle = mk_coords(new_coords_width); | |
| core::ir::Value* last = b.Swizzle(coords_ty->Type(), coords, Vector{new_coords_width}); | |
| if (is_proj) { | |
| // New coords | |
| // Divide the coordinates by the last value to simulate the | |
| // projection behaviour. | |
| new_args.Push(b.Divide(swizzle, last)); | |
| } else { | |
| TINT_ASSERT(new_coords_ty->Is<core::type::Vector>()); | |
| // New coords | |
| new_args.Push(swizzle); | |
| // Array index | |
| if (!last->Type()->Is<core::type::I32>()) { | |
| last = b.Convert(ty.i32(), last); | |
| } | |
| new_args.Push(last); | |
| } | |
| } | |
| void ProcessOffset(core::ir::Value* offset, Vector<core::ir::Value*, 5>& new_args) { | |
| if (offset->Type()->IsSignedIntegerVector()) { | |
| new_args.Push(offset); | |
| return; | |
| } | |
| auto* vec_ty = offset->Type()->As<core::type::Vector>(); | |
| TINT_ASSERT(vec_ty); | |
| auto* ir_constant = offset->As<core::ir::Constant>(); | |
| TINT_ASSERT(ir_constant); | |
| auto* constant_value = ir_constant->Value(); | |
| Vector<const core::constant::Value*, 4> new_values; | |
| for (size_t i = 0; i < vec_ty->Width(); ++i) { | |
| uint32_t v = constant_value->Index(i)->ValueAs<uint32_t>(); | |
| new_values.Push(b.ConstantValue(i32(v))); | |
| } | |
| auto* new_offset = b.Composite(ty.vec(ty.i32(), vec_ty->Width()), new_values); | |
| new_args.Push(new_offset); | |
| } | |
| uint32_t GetOperandMask(core::ir::Value* val) { | |
| auto* op = val->As<core::ir::Constant>(); | |
| TINT_ASSERT(op); | |
| return op->Value()->ValueAs<uint32_t>(); | |
| } | |
| bool HasBias(uint32_t mask) { | |
| return (mask & static_cast<uint32_t>(ImageOperandsMask::kBias)) != 0; | |
| } | |
| bool HasGrad(uint32_t mask) { | |
| return (mask & static_cast<uint32_t>(ImageOperandsMask::kGrad)) != 0; | |
| } | |
| bool HasLod(uint32_t mask) { | |
| return (mask & static_cast<uint32_t>(ImageOperandsMask::kLod)) != 0; | |
| } | |
| bool HasConstOffset(uint32_t mask) { | |
| return (mask & static_cast<uint32_t>(ImageOperandsMask::kConstOffset)) != 0; | |
| } | |
| bool HasSample(uint32_t mask) { | |
| return (mask & static_cast<uint32_t>(ImageOperandsMask::kSample)) != 0; | |
| } | |
| void Image(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| core::ir::Value* tex = nullptr; | |
| [[maybe_unused]] core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(args[0]); | |
| call->Result()->ReplaceAllUsesWith(tex); | |
| call->Destroy(); | |
| } | |
| void ImageFetch(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| b.InsertBefore(call, [&] { | |
| core::ir::Value* tex = args[0]; | |
| auto* coords = args[1]; | |
| auto* tex_ty = tex->Type(); | |
| uint32_t operand_mask = GetOperandMask(args[2]); | |
| Vector<core::ir::Value*, 5> new_args = {tex}; | |
| ProcessCoords(tex->Type(), false, coords, new_args); | |
| uint32_t idx = 3; | |
| if (HasLod(operand_mask)) { | |
| core::ir::Value* lod = args[idx++]; | |
| if (!lod->Type()->Is<core::type::I32>()) { | |
| lod = b.Convert(ty.i32(), lod); | |
| } | |
| new_args.Push(lod); | |
| } else if (!tex_ty->IsAnyOf<core::type::DepthMultisampledTexture, | |
| core::type::MultisampledTexture, | |
| core::type::StorageTexture>()) { | |
| // textureLoad requires an explicit level-of-detail parameter for non-multisampled | |
| // and non-storage texture types. | |
| new_args.Push(b.Zero(ty.i32())); | |
| } | |
| if (HasSample(operand_mask)) { | |
| core::ir::Value* sample = args[idx++]; | |
| if (!sample->Type()->Is<core::type::I32>()) { | |
| sample = b.Convert(ty.i32(), sample); | |
| } | |
| new_args.Push(sample); | |
| } | |
| // Depth textures have a single value return in WGSL, but a vec4 in SPIR-V. | |
| auto* call_ty = call->Result()->Type(); | |
| if (tex_ty->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) { | |
| call_ty = call_ty->DeepestElement(); | |
| } | |
| auto* res = b.Call(call_ty, core::BuiltinFn::kTextureLoad, new_args); | |
| // Restore the vec4 result by padding with 0's. | |
| if (call_ty != call->Result()->Type()) { | |
| auto* vec = call->Result()->Type()->As<core::type::Vector>(); | |
| TINT_ASSERT(vec && vec->Width() == 4); | |
| auto* z = b.Zero(call_ty); | |
| res = b.Construct(call->Result()->Type(), res, z, z, z); | |
| } | |
| call->Result()->ReplaceAllUsesWith(res); | |
| }); | |
| call->Destroy(); | |
| } | |
| void FindVarsForImageGatherDref(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| core::ir::Value* tex = nullptr; | |
| core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(args[0]); | |
| textures_to_convert_to_depth_.Push(tex); | |
| samplers_to_convert_to_comparison_.Push(sampler); | |
| } | |
| void ImageGatherDref(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| b.InsertBefore(call, [&] { | |
| core::ir::Value* tex = nullptr; | |
| core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(args[0]); | |
| auto* coords = args[1]; | |
| auto* dref = args[2]; | |
| uint32_t operand_mask = GetOperandMask(args[3]); | |
| Vector<core::ir::Value*, 5> new_args; | |
| new_args.Push(tex); | |
| new_args.Push(sampler); | |
| ProcessCoords(tex->Type(), false, coords, new_args); | |
| new_args.Push(dref); | |
| if (HasConstOffset(operand_mask)) { | |
| ProcessOffset(args[4], new_args); | |
| } | |
| b.CallReplaceResult(call->DetachResult(), core::BuiltinFn::kTextureGatherCompare, | |
| new_args); | |
| }); | |
| call->Destroy(); | |
| } | |
| void ImageGather(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| b.InsertBefore(call, [&] { | |
| core::ir::Value* tex = nullptr; | |
| core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(args[0]); | |
| auto* coords = args[1]; | |
| auto* component = args[2]; | |
| uint32_t operand_mask = GetOperandMask(args[3]); | |
| Vector<core::ir::Value*, 5> new_args; | |
| if (!tex->Type()->Is<core::type::DepthTexture>()) { | |
| new_args.Push(component); | |
| } | |
| new_args.Push(tex); | |
| new_args.Push(sampler); | |
| ProcessCoords(tex->Type(), false, coords, new_args); | |
| if (HasConstOffset(operand_mask)) { | |
| ProcessOffset(args[4], new_args); | |
| } | |
| b.CallReplaceResult(call->DetachResult(), core::BuiltinFn::kTextureGather, new_args); | |
| }); | |
| call->Destroy(); | |
| } | |
| void FindVarsForImageSampleDref(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| auto* sampled_image = args[0]; | |
| core::ir::Value* tex = nullptr; | |
| core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(sampled_image); | |
| textures_to_convert_to_depth_.Push(tex); | |
| samplers_to_convert_to_comparison_.Push(sampler); | |
| } | |
| void ImageSampleDref(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| auto* sampled_image = args[0]; | |
| core::ir::Value* tex = nullptr; | |
| core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(sampled_image); | |
| auto* tex_ty = tex->Type(); | |
| auto* coords = args[1]; | |
| auto* depth = args[2]; | |
| uint32_t operand_mask = GetOperandMask(args[3]); | |
| bool is_proj = call->Func() == spirv::BuiltinFn::kImageSampleProjDrefImplicitLod || | |
| call->Func() == spirv::BuiltinFn::kImageSampleProjDrefExplicitLod; | |
| uint32_t idx = 4; | |
| b.InsertBefore(call, [&] { | |
| Vector<core::ir::Value*, 5> new_args; | |
| new_args.Push(tex); | |
| new_args.Push(sampler); | |
| ProcessCoords(tex_ty, is_proj, coords, new_args); | |
| new_args.Push(depth); | |
| auto fn = core::BuiltinFn::kTextureSampleCompare; | |
| if (HasLod(operand_mask)) { | |
| fn = core::BuiltinFn::kTextureSampleCompareLevel; | |
| idx++; // Skip over the index | |
| // Metal only supports Lod = 0 for comparison sampling without derivatives. So, WGSL | |
| // doesn't take a level value, drop the LOD param. | |
| } | |
| if (HasConstOffset(operand_mask)) { | |
| ProcessOffset(args[idx++], new_args); | |
| } | |
| b.CallReplaceResult(call->DetachResult(), fn, new_args); | |
| }); | |
| call->Destroy(); | |
| } | |
| void ImageSample(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| auto* sampled_image = args[0]; | |
| core::ir::Value* tex = nullptr; | |
| core::ir::Value* sampler = nullptr; | |
| std::tie(tex, sampler) = GetTextureSampler(sampled_image); | |
| auto* tex_ty = tex->Type(); | |
| auto* coords = args[1]; | |
| uint32_t operand_mask = GetOperandMask(args[2]); | |
| bool is_proj = call->Func() == spirv::BuiltinFn::kImageSampleProjImplicitLod || | |
| call->Func() == spirv::BuiltinFn::kImageSampleProjExplicitLod; | |
| uint32_t idx = 3; | |
| b.InsertBefore(call, [&] { | |
| Vector<core::ir::Value*, 5> new_args; | |
| new_args.Push(tex); | |
| new_args.Push(sampler); | |
| ProcessCoords(tex_ty, is_proj, coords, new_args); | |
| core::BuiltinFn fn = core::BuiltinFn::kTextureSample; | |
| if (HasBias(operand_mask)) { | |
| fn = core::BuiltinFn::kTextureSampleBias; | |
| new_args.Push(args[idx++]); | |
| } | |
| if (HasLod(operand_mask)) { | |
| fn = core::BuiltinFn::kTextureSampleLevel; | |
| core::ir::Value* lod = args[idx++]; | |
| // Depth texture LOD in WGSL is i32/u32 but f32 in SPIR-V. | |
| // Convert to i32 | |
| if (tex_ty->Is<core::type::DepthTexture>()) { | |
| lod = b.Convert(ty.i32(), lod); | |
| } | |
| new_args.Push(lod); | |
| } | |
| if (HasGrad(operand_mask)) { | |
| fn = core::BuiltinFn::kTextureSampleGrad; | |
| new_args.Push(args[idx++]); // ddx | |
| new_args.Push(args[idx++]); // ddy | |
| } | |
| if (HasConstOffset(operand_mask)) { | |
| ProcessOffset(args[idx++], new_args); | |
| } | |
| // Depth textures have a single value return in WGSL, but a vec4 in SPIR-V. | |
| auto* call_ty = call->Result()->Type(); | |
| if (tex_ty->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) { | |
| call_ty = call_ty->DeepestElement(); | |
| } | |
| auto* res = b.Call(call_ty, fn, new_args); | |
| // Restore the vec4 result by padding with 0's. | |
| if (call_ty != call->Result()->Type()) { | |
| auto* vec = call->Result()->Type()->As<core::type::Vector>(); | |
| TINT_ASSERT(vec && vec->Width() == 4); | |
| auto* z = b.Zero(call_ty); | |
| res = b.Construct(call->Result()->Type(), res, z, z, z); | |
| } | |
| call->Result()->ReplaceAllUsesWith(res); | |
| }); | |
| call->Destroy(); | |
| } | |
| void ImageWrite(spirv::ir::BuiltinCall* call) { | |
| const auto& args = call->Args(); | |
| b.InsertBefore(call, [&] { | |
| core::ir::Value* tex = args[0]; | |
| auto* coords = args[1]; | |
| auto* texel = args[2]; | |
| Vector<core::ir::Value*, 5> new_args; | |
| new_args.Push(tex); | |
| ProcessCoords(tex->Type(), false, coords, new_args); | |
| new_args.Push(texel); | |
| b.Call(call->Result()->Type(), core::BuiltinFn::kTextureStore, new_args); | |
| }); | |
| call->Destroy(); | |
| } | |
| void ImageQuery(spirv::ir::BuiltinCall* call, core::BuiltinFn fn) { | |
| auto* image = call->Args()[0]; | |
| b.InsertBefore(call, [&] { | |
| auto* type = call->Result()->Type(); | |
| // WGSL requires a `u32` result component where SPIR-V allows `i32` or `u32` | |
| core::ir::Value* res = b.Call(ty.MatchWidth(ty.u32(), type), fn, Vector{image}); | |
| if (type->IsSignedIntegerScalarOrVector()) { | |
| res = b.Convert(type, res); | |
| } | |
| call->Result()->ReplaceAllUsesWith(res); | |
| }); | |
| call->Destroy(); | |
| } | |
| void ImageQuerySize(spirv::ir::BuiltinCall* call) { | |
| auto* image = call->Args()[0]; | |
| auto* tex_ty = image->Type()->As<core::type::Texture>(); | |
| TINT_ASSERT(tex_ty); | |
| b.InsertBefore(call, [&] { | |
| auto* type = call->Result()->Type(); | |
| // WGSL requires a `u32` result component where SPIR-V allows `i32` or `u32` | |
| auto* wgsl_type = ty.MatchWidth(ty.u32(), type); | |
| // A SPIR-V OpImageQuery will return the array `element` entry with | |
| // the image query. In WGSL, these are two calls, the | |
| // `textureDimensions` will get the width,height,depth but we also | |
| // have to call `textureNumLayers` to get the elements and then | |
| // re-inject that back into the result. | |
| if (core::type::IsTextureArray(tex_ty->Dim())) { | |
| wgsl_type = ty.vec(ty.u32(), wgsl_type->As<core::type::Vector>()->Width() - 1); | |
| } | |
| Vector<core::ir::Value*, 2> args = {image}; | |
| if (call->Func() == spirv::BuiltinFn::kImageQuerySizeLod) { | |
| args.Push(call->Args()[1]); | |
| } | |
| core::ir::Value* res = b.Call(wgsl_type, core::BuiltinFn::kTextureDimensions, args); | |
| if (core::type::IsTextureArray(tex_ty->Dim())) { | |
| core::ir::Value* layers = | |
| b.Call(ty.u32(), core::BuiltinFn::kTextureNumLayers, image); | |
| res = b.Construct(ty.MatchWidth(ty.u32(), type), res, layers); | |
| } | |
| if (type->IsSignedIntegerScalarOrVector()) { | |
| res = b.Convert(type, res); | |
| } | |
| call->Result()->ReplaceAllUsesWith(res); | |
| }); | |
| call->Destroy(); | |
| } | |
| const core::type::Type* TypeFor(const core::type::Type* src_ty) { | |
| if (auto* img = src_ty->As<spirv::type::Image>()) { | |
| return TypeForImage(img); | |
| } | |
| return src_ty; | |
| } | |
| core::type::TextureDimension ConvertDim(spirv::type::Dim dim, spirv::type::Arrayed arrayed) { | |
| switch (dim) { | |
| case spirv::type::Dim::kD1: | |
| return core::type::TextureDimension::k1d; | |
| case spirv::type::Dim::kD2: | |
| return arrayed == spirv::type::Arrayed::kArrayed | |
| ? core::type::TextureDimension::k2dArray | |
| : core::type::TextureDimension::k2d; | |
| case spirv::type::Dim::kD3: | |
| return core::type::TextureDimension::k3d; | |
| case spirv::type::Dim::kCube: | |
| return arrayed == spirv::type::Arrayed::kArrayed | |
| ? core::type::TextureDimension::kCubeArray | |
| : core::type::TextureDimension::kCube; | |
| default: | |
| TINT_UNREACHABLE(); | |
| } | |
| } | |
| const core::type::Type* TypeForImage(const spirv::type::Image* img) { | |
| if (img->GetDim() == spirv::type::Dim::kSubpassData) { | |
| return ty.input_attachment(img->GetSampledType()); | |
| } | |
| if (img->GetSampled() == spirv::type::Sampled::kReadWriteOpCompatible) { | |
| return ty.storage_texture(ConvertDim(img->GetDim(), img->GetArrayed()), | |
| img->GetTexelFormat(), img->GetAccess()); | |
| } | |
| if (img->GetDepth() == spirv::type::Depth::kDepth) { | |
| if (img->GetMultisampled() == spirv::type::Multisampled::kMultisampled) { | |
| return ty.depth_multisampled_texture(ConvertDim(img->GetDim(), img->GetArrayed())); | |
| } | |
| return ty.depth_texture(ConvertDim(img->GetDim(), img->GetArrayed())); | |
| } | |
| if (img->GetMultisampled() == spirv::type::Multisampled::kMultisampled) { | |
| return ty.multisampled_texture(ConvertDim(img->GetDim(), img->GetArrayed()), | |
| img->GetSampledType()); | |
| } | |
| return ty.sampled_texture(ConvertDim(img->GetDim(), img->GetArrayed()), | |
| img->GetSampledType()); | |
| } | |
| }; | |
| } // namespace | |
| Result<SuccessType> Texture(core::ir::Module& ir) { | |
| AssertValid(ir, "before spirv.Texture"); | |
| State{ir}.Process(); | |
| ir.properties.Remove(core::ir::Property::kAllowPointerToHandle); | |
| return Success; | |
| } | |
| } // namespace tint::spirv::reader::lower |