blob: 19a5662ec0f080fe2e515262ac00b14d7820de69 [file] [log] [blame]
// Copyright 2020 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/writer/ast_printer/builder.h"
#include <algorithm>
#include <limits>
#include <utility>
#include "spirv/unified1/GLSL.std.450.h"
#include "src/tint/lang/core/constant/value.h"
#include "src/tint/lang/core/fluent_types.h"
#include "src/tint/lang/core/type/array.h"
#include "src/tint/lang/core/type/atomic.h"
#include "src/tint/lang/core/type/depth_multisampled_texture.h"
#include "src/tint/lang/core/type/depth_texture.h"
#include "src/tint/lang/core/type/multisampled_texture.h"
#include "src/tint/lang/core/type/reference.h"
#include "src/tint/lang/core/type/sampled_texture.h"
#include "src/tint/lang/core/type/texture_dimension.h"
#include "src/tint/lang/core/type/vector.h"
#include "src/tint/lang/wgsl/ast/call_statement.h"
#include "src/tint/lang/wgsl/ast/id_attribute.h"
#include "src/tint/lang/wgsl/ast/internal_attribute.h"
#include "src/tint/lang/wgsl/ast/transform/add_block_attribute.h"
#include "src/tint/lang/wgsl/ast/traverse_expressions.h"
#include "src/tint/lang/wgsl/helpers/append_vector.h"
#include "src/tint/lang/wgsl/helpers/check_supported_extensions.h"
#include "src/tint/lang/wgsl/sem/builtin_fn.h"
#include "src/tint/lang/wgsl/sem/call.h"
#include "src/tint/lang/wgsl/sem/function.h"
#include "src/tint/lang/wgsl/sem/load.h"
#include "src/tint/lang/wgsl/sem/materialize.h"
#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
#include "src/tint/lang/wgsl/sem/module.h"
#include "src/tint/lang/wgsl/sem/statement.h"
#include "src/tint/lang/wgsl/sem/struct.h"
#include "src/tint/lang/wgsl/sem/switch_statement.h"
#include "src/tint/lang/wgsl/sem/value_constructor.h"
#include "src/tint/lang/wgsl/sem/value_conversion.h"
#include "src/tint/lang/wgsl/sem/variable.h"
#include "src/tint/utils/containers/map.h"
#include "src/tint/utils/macros/compiler.h"
#include "src/tint/utils/macros/defer.h"
#include "src/tint/utils/text/string_stream.h"
using namespace tint::core::fluent_types; // NOLINT
namespace tint::spirv::writer {
namespace {
const char kGLSLstd450[] = "GLSL.std.450";
uint32_t pipeline_stage_to_execution_model(ast::PipelineStage stage) {
SpvExecutionModel model = SpvExecutionModelVertex;
switch (stage) {
case ast::PipelineStage::kFragment:
model = SpvExecutionModelFragment;
break;
case ast::PipelineStage::kVertex:
model = SpvExecutionModelVertex;
break;
case ast::PipelineStage::kCompute:
model = SpvExecutionModelGLCompute;
break;
case ast::PipelineStage::kNone:
model = SpvExecutionModelMax;
break;
}
return model;
}
/// Returns the matrix type that is `type` or that is wrapped by
/// one or more levels of an arrays inside of `type`.
/// @param type the given type, which must not be null
/// @returns the nested matrix type, or nullptr if none
const core::type::Matrix* GetNestedMatrixType(const core::type::Type* type) {
while (auto* arr = type->As<core::type::Array>()) {
type = arr->ElemType();
}
return type->As<core::type::Matrix>();
}
uint32_t builtin_to_glsl_method(const sem::BuiltinFn* builtin) {
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kAcos:
return GLSLstd450Acos;
case wgsl::BuiltinFn::kAcosh:
return GLSLstd450Acosh;
case wgsl::BuiltinFn::kAsin:
return GLSLstd450Asin;
case wgsl::BuiltinFn::kAsinh:
return GLSLstd450Asinh;
case wgsl::BuiltinFn::kAtan:
return GLSLstd450Atan;
case wgsl::BuiltinFn::kAtan2:
return GLSLstd450Atan2;
case wgsl::BuiltinFn::kAtanh:
return GLSLstd450Atanh;
case wgsl::BuiltinFn::kCeil:
return GLSLstd450Ceil;
case wgsl::BuiltinFn::kClamp:
if (builtin->ReturnType()->is_float_scalar_or_vector()) {
return GLSLstd450NClamp;
} else if (builtin->ReturnType()->is_unsigned_integer_scalar_or_vector()) {
return GLSLstd450UClamp;
} else {
return GLSLstd450SClamp;
}
case wgsl::BuiltinFn::kCos:
return GLSLstd450Cos;
case wgsl::BuiltinFn::kCosh:
return GLSLstd450Cosh;
case wgsl::BuiltinFn::kCross:
return GLSLstd450Cross;
case wgsl::BuiltinFn::kDegrees:
return GLSLstd450Degrees;
case wgsl::BuiltinFn::kDeterminant:
return GLSLstd450Determinant;
case wgsl::BuiltinFn::kDistance:
return GLSLstd450Distance;
case wgsl::BuiltinFn::kExp:
return GLSLstd450Exp;
case wgsl::BuiltinFn::kExp2:
return GLSLstd450Exp2;
case wgsl::BuiltinFn::kFaceForward:
return GLSLstd450FaceForward;
case wgsl::BuiltinFn::kFloor:
return GLSLstd450Floor;
case wgsl::BuiltinFn::kFma:
return GLSLstd450Fma;
case wgsl::BuiltinFn::kFract:
return GLSLstd450Fract;
case wgsl::BuiltinFn::kFrexp:
return GLSLstd450FrexpStruct;
case wgsl::BuiltinFn::kInverseSqrt:
return GLSLstd450InverseSqrt;
case wgsl::BuiltinFn::kLdexp:
return GLSLstd450Ldexp;
case wgsl::BuiltinFn::kLength:
return GLSLstd450Length;
case wgsl::BuiltinFn::kLog:
return GLSLstd450Log;
case wgsl::BuiltinFn::kLog2:
return GLSLstd450Log2;
case wgsl::BuiltinFn::kMax:
if (builtin->ReturnType()->is_float_scalar_or_vector()) {
return GLSLstd450NMax;
} else if (builtin->ReturnType()->is_unsigned_integer_scalar_or_vector()) {
return GLSLstd450UMax;
} else {
return GLSLstd450SMax;
}
case wgsl::BuiltinFn::kMin:
if (builtin->ReturnType()->is_float_scalar_or_vector()) {
return GLSLstd450NMin;
} else if (builtin->ReturnType()->is_unsigned_integer_scalar_or_vector()) {
return GLSLstd450UMin;
} else {
return GLSLstd450SMin;
}
case wgsl::BuiltinFn::kMix:
return GLSLstd450FMix;
case wgsl::BuiltinFn::kModf:
return GLSLstd450ModfStruct;
case wgsl::BuiltinFn::kNormalize:
return GLSLstd450Normalize;
case wgsl::BuiltinFn::kPack4X8Snorm:
return GLSLstd450PackSnorm4x8;
case wgsl::BuiltinFn::kPack4X8Unorm:
return GLSLstd450PackUnorm4x8;
case wgsl::BuiltinFn::kPack2X16Snorm:
return GLSLstd450PackSnorm2x16;
case wgsl::BuiltinFn::kPack2X16Unorm:
return GLSLstd450PackUnorm2x16;
case wgsl::BuiltinFn::kPack2X16Float:
return GLSLstd450PackHalf2x16;
case wgsl::BuiltinFn::kPow:
return GLSLstd450Pow;
case wgsl::BuiltinFn::kRadians:
return GLSLstd450Radians;
case wgsl::BuiltinFn::kReflect:
return GLSLstd450Reflect;
case wgsl::BuiltinFn::kRefract:
return GLSLstd450Refract;
case wgsl::BuiltinFn::kRound:
return GLSLstd450RoundEven;
case wgsl::BuiltinFn::kSign:
if (builtin->ReturnType()->is_signed_integer_scalar_or_vector()) {
return GLSLstd450SSign;
} else {
return GLSLstd450FSign;
}
case wgsl::BuiltinFn::kSin:
return GLSLstd450Sin;
case wgsl::BuiltinFn::kSinh:
return GLSLstd450Sinh;
case wgsl::BuiltinFn::kSmoothstep:
return GLSLstd450SmoothStep;
case wgsl::BuiltinFn::kSqrt:
return GLSLstd450Sqrt;
case wgsl::BuiltinFn::kStep:
return GLSLstd450Step;
case wgsl::BuiltinFn::kTan:
return GLSLstd450Tan;
case wgsl::BuiltinFn::kTanh:
return GLSLstd450Tanh;
case wgsl::BuiltinFn::kTrunc:
return GLSLstd450Trunc;
case wgsl::BuiltinFn::kUnpack4X8Snorm:
return GLSLstd450UnpackSnorm4x8;
case wgsl::BuiltinFn::kUnpack4X8Unorm:
return GLSLstd450UnpackUnorm4x8;
case wgsl::BuiltinFn::kUnpack2X16Snorm:
return GLSLstd450UnpackSnorm2x16;
case wgsl::BuiltinFn::kUnpack2X16Unorm:
return GLSLstd450UnpackUnorm2x16;
case wgsl::BuiltinFn::kUnpack2X16Float:
return GLSLstd450UnpackHalf2x16;
default:
break;
}
return 0;
}
/// @return the vector element type if ty is a vector, otherwise return ty.
const core::type::Type* ElementTypeOf(const core::type::Type* ty) {
if (auto* v = ty->As<core::type::Vector>()) {
return v->type();
}
return ty;
}
} // namespace
Builder::AccessorInfo::AccessorInfo() : source_id(0), source_type(nullptr) {}
Builder::AccessorInfo::~AccessorInfo() {}
Builder::Builder(const Program& program,
bool zero_initialize_workgroup_memory,
bool experimental_require_subgroup_uniform_control_flow)
: builder_(ProgramBuilder::Wrap(program)),
scope_stack_{Scope{}},
zero_initialize_workgroup_memory_(zero_initialize_workgroup_memory),
experimental_require_subgroup_uniform_control_flow_(
experimental_require_subgroup_uniform_control_flow) {}
Builder::~Builder() = default;
bool Builder::Build() {
if (!tint::wgsl::CheckSupportedExtensions(
"SPIR-V", builder_.AST(), builder_.Diagnostics(),
Vector{
wgsl::Extension::kChromiumDisableUniformityAnalysis,
wgsl::Extension::kChromiumExperimentalFullPtrParameters,
wgsl::Extension::kChromiumExperimentalPushConstant,
wgsl::Extension::kChromiumExperimentalSubgroups,
wgsl::Extension::kF16,
wgsl::Extension::kChromiumInternalDualSourceBlending,
})) {
return false;
}
module_.PushCapability(SpvCapabilityShader);
module_.PushMemoryModel(spv::Op::OpMemoryModel, {U32Operand(SpvAddressingModelLogical),
U32Operand(SpvMemoryModelGLSL450)});
for (auto ext : builder_.Sem().Module()->Extensions()) {
GenerateExtension(ext);
}
// Emit SPV_KHR_subgroup_uniform_control_flow extension if required.
if (experimental_require_subgroup_uniform_control_flow_) {
module_.PushExtension("SPV_KHR_subgroup_uniform_control_flow");
}
for (auto* var : builder_.AST().GlobalVariables()) {
if (!GenerateGlobalVariable(var)) {
return false;
}
}
auto* mod = builder_.Sem().Module();
for (auto* decl : mod->DependencyOrderedDeclarations()) {
if (auto* func = decl->As<ast::Function>()) {
if (!GenerateFunction(func)) {
return false;
}
}
}
return true;
}
void Builder::RegisterVariable(const sem::Variable* var, uint32_t id) {
var_to_id_.emplace(var, id);
id_to_var_.emplace(id, var);
}
uint32_t Builder::LookupVariableID(const sem::Variable* var) {
auto it = var_to_id_.find(var);
if (it == var_to_id_.end()) {
TINT_ICE() << "unable to find ID for variable: " + var->Declaration()->name->symbol.Name();
return 0;
}
return it->second;
}
void Builder::PushScope() {
// Push a new scope, by copying the top-most stack
scope_stack_.push_back(scope_stack_.back());
}
void Builder::PopScope() {
scope_stack_.pop_back();
}
Operand Builder::result_op() {
return Operand(module_.NextId());
}
bool Builder::GenerateExtension(wgsl::Extension extension) {
switch (extension) {
case wgsl::Extension::kF16:
module_.PushCapability(SpvCapabilityFloat16);
module_.PushCapability(SpvCapabilityUniformAndStorageBuffer16BitAccess);
module_.PushCapability(SpvCapabilityStorageBuffer16BitAccess);
module_.PushCapability(SpvCapabilityStorageInputOutput16);
break;
default:
return false;
}
return true;
}
bool Builder::GenerateLabel(uint32_t id) {
if (!push_function_inst(spv::Op::OpLabel, {Operand(id)})) {
return false;
}
current_label_id_ = id;
return true;
}
bool Builder::GenerateAssignStatement(const ast::AssignmentStatement* assign) {
if (assign->lhs->Is<ast::PhonyExpression>()) {
if (builder_.Sem().GetVal(assign->rhs)->ConstantValue()) {
// RHS of phony assignment is constant.
// Constants can't have side-effects, so just drop this.
return true;
}
auto rhs_id = GenerateExpression(assign->rhs);
if (rhs_id == 0) {
return false;
}
return true;
} else {
auto lhs_id = GenerateExpression(assign->lhs);
if (lhs_id == 0) {
return false;
}
auto rhs_id = GenerateExpression(assign->rhs);
if (rhs_id == 0) {
return false;
}
return GenerateStore(lhs_id, rhs_id);
}
}
bool Builder::GenerateBreakStatement(const ast::BreakStatement*) {
if (merge_stack_.empty()) {
TINT_ICE() << "Attempted to break without a merge block";
return false;
}
if (!push_function_inst(spv::Op::OpBranch, {Operand(merge_stack_.back())})) {
return false;
}
return true;
}
bool Builder::GenerateBreakIfStatement(const ast::BreakIfStatement* stmt) {
TINT_ASSERT(!backedge_stack_.empty());
const auto cond_id = GenerateExpression(stmt->condition);
if (!cond_id) {
return false;
}
const ContinuingInfo& ci = continuing_stack_.back();
backedge_stack_.back() =
Backedge(spv::Op::OpBranchConditional,
{Operand(cond_id), Operand(ci.break_target_id), Operand(ci.loop_header_id)});
return true;
}
bool Builder::GenerateContinueStatement(const ast::ContinueStatement*) {
if (continue_stack_.empty()) {
TINT_ICE() << "Attempted to continue without a continue block";
return false;
}
if (!push_function_inst(spv::Op::OpBranch, {Operand(continue_stack_.back())})) {
return false;
}
return true;
}
// TODO(dsinclair): This is generating an OpKill but the semantics of kill
// haven't been defined for WGSL yet. So, this may need to change.
// https://github.com/gpuweb/gpuweb/issues/676
bool Builder::GenerateDiscardStatement(const ast::DiscardStatement*) {
if (!push_function_inst(spv::Op::OpKill, {})) {
return false;
}
return true;
}
bool Builder::GenerateEntryPoint(const ast::Function* func, uint32_t id) {
auto stage = pipeline_stage_to_execution_model(func->PipelineStage());
if (stage == SpvExecutionModelMax) {
TINT_ICE() << "Unknown pipeline stage provided";
return false;
}
OperandList operands = {Operand(stage), Operand(id), Operand(func->name->symbol.Name())};
auto* func_sem = builder_.Sem().Get(func);
for (const auto* var : func_sem->TransitivelyReferencedGlobals()) {
// For SPIR-V 1.3 we only output Input/output variables. If we update to
// SPIR-V 1.4 or later this should be all variables.
if (var->AddressSpace() != core::AddressSpace::kIn &&
var->AddressSpace() != core::AddressSpace::kOut) {
continue;
}
uint32_t var_id = LookupVariableID(var);
if (var_id == 0) {
TINT_ICE() << "unable to find ID for global variable: " +
var->Declaration()->name->symbol.Name();
return false;
}
operands.push_back(Operand(var_id));
}
module_.PushEntryPoint(spv::Op::OpEntryPoint, operands);
return true;
}
bool Builder::GenerateExecutionModes(const ast::Function* func, uint32_t id) {
auto* func_sem = builder_.Sem().Get(func);
// WGSL fragment shader origin is upper left
if (func->PipelineStage() == ast::PipelineStage::kFragment) {
module_.PushExecutionMode(spv::Op::OpExecutionMode,
{Operand(id), U32Operand(SpvExecutionModeOriginUpperLeft)});
} else if (func->PipelineStage() == ast::PipelineStage::kCompute) {
auto& wgsize = func_sem->WorkgroupSize();
// Check if the workgroup_size uses pipeline-overridable constants.
if (!wgsize[0].has_value() || !wgsize[1].has_value() || !wgsize[2].has_value()) {
TINT_ICE()
<< "override-expressions should have been removed with the SubstituteOverride "
"transform";
return false;
}
module_.PushExecutionMode(
spv::Op::OpExecutionMode,
{Operand(id), U32Operand(SpvExecutionModeLocalSize), //
Operand(wgsize[0].value()), Operand(wgsize[1].value()), Operand(wgsize[2].value())});
}
for (auto it : func_sem->TransitivelyReferencedBuiltinVariables()) {
auto builtin = builder_.Sem().Get(it.second)->Value();
if (builtin == core::BuiltinValue::kFragDepth) {
module_.PushExecutionMode(spv::Op::OpExecutionMode,
{Operand(id), U32Operand(SpvExecutionModeDepthReplacing)});
break;
}
}
// Use SubgroupUniformControlFlow execution mode for compute stage if required.
if (experimental_require_subgroup_uniform_control_flow_ &&
func->PipelineStage() == ast::PipelineStage::kCompute) {
module_.PushExecutionMode(
spv::Op::OpExecutionMode,
{Operand(id), U32Operand(SpvExecutionModeSubgroupUniformControlFlowKHR)});
}
return true;
}
uint32_t Builder::GenerateExpression(const sem::Expression* expr) {
if (auto* val_expr = expr->As<sem::ValueExpression>()) {
if (auto* constant = val_expr->ConstantValue()) {
return GenerateConstantIfNeeded(constant);
}
}
if (auto* load = expr->As<sem::Load>()) {
auto ref_id = GenerateExpression(load->Reference());
if (ref_id == 0) {
return 0;
}
return GenerateLoad(load->ReferenceType(), ref_id);
}
return Switch(
expr->Declaration(), //
[&](const ast::AccessorExpression* a) { return GenerateAccessorExpression(a); },
[&](const ast::BinaryExpression* b) { return GenerateBinaryExpression(b); },
[&](const ast::BitcastExpression* b) { return GenerateBitcastExpression(b); },
[&](const ast::CallExpression* c) { return GenerateCallExpression(c); },
[&](const ast::IdentifierExpression* i) { return GenerateIdentifierExpression(i); },
[&](const ast::LiteralExpression* l) { return GenerateLiteralIfNeeded(l); },
[&](const ast::UnaryOpExpression* u) { return GenerateUnaryOpExpression(u); }, //
TINT_ICE_ON_NO_MATCH);
}
uint32_t Builder::GenerateExpression(const ast::Expression* expr) {
return GenerateExpression(builder_.Sem().Get(expr));
}
bool Builder::GenerateFunction(const ast::Function* func_ast) {
auto* func = builder_.Sem().Get(func_ast);
uint32_t func_type_id = GenerateFunctionTypeIfNeeded(func);
if (func_type_id == 0) {
return false;
}
auto func_op = result_op();
auto func_id = std::get<uint32_t>(func_op);
module_.PushDebug(spv::Op::OpName, {Operand(func_id), Operand(func_ast->name->symbol.Name())});
auto ret_id = GenerateTypeIfNeeded(func->ReturnType());
if (ret_id == 0) {
return false;
}
PushScope();
TINT_DEFER(PopScope());
auto definition_inst = Instruction{
spv::Op::OpFunction,
{Operand(ret_id), func_op, U32Operand(SpvFunctionControlMaskNone), Operand(func_type_id)}};
InstructionList params;
for (auto* param : func->Parameters()) {
auto param_op = result_op();
auto param_id = std::get<uint32_t>(param_op);
auto param_type_id = GenerateTypeIfNeeded(param->Type());
if (param_type_id == 0) {
return false;
}
module_.PushDebug(spv::Op::OpName,
{Operand(param_id), Operand(param->Declaration()->name->symbol.Name())});
params.push_back(
Instruction{spv::Op::OpFunctionParameter, {Operand(param_type_id), param_op}});
RegisterVariable(param, param_id);
}
// Start a new function.
current_function_ = Function{definition_inst, result_op(), std::move(params)};
current_label_id_ = current_function_.label_id();
TINT_DEFER(current_function_ = Function());
for (auto* stmt : func_ast->body->statements) {
if (!GenerateStatement(stmt)) {
return false;
}
}
if (InsideBasicBlock()) {
if (func->ReturnType()->Is<core::type::Void>()) {
push_function_inst(spv::Op::OpReturn, {});
} else {
auto zero = GenerateConstantNullIfNeeded(func->ReturnType());
push_function_inst(spv::Op::OpReturnValue, {Operand(zero)});
}
}
if (func_ast->IsEntryPoint()) {
if (!GenerateEntryPoint(func_ast, func_id)) {
return false;
}
if (!GenerateExecutionModes(func_ast, func_id)) {
return false;
}
}
func_symbol_to_id_[func_ast->name->symbol] = func_id;
// Add the function to the module.
module_.PushFunction(std::move(current_function_));
return true;
}
uint32_t Builder::GenerateFunctionTypeIfNeeded(const sem::Function* func) {
return tint::GetOrCreate(func_sig_to_id_, func->Signature(), [&]() -> uint32_t {
auto func_op = result_op();
auto func_type_id = std::get<uint32_t>(func_op);
auto ret_id = GenerateTypeIfNeeded(func->ReturnType());
if (ret_id == 0) {
return 0;
}
OperandList ops = {func_op, Operand(ret_id)};
for (auto* param : func->Parameters()) {
auto param_type_id = GenerateTypeIfNeeded(param->Type());
if (param_type_id == 0) {
return 0;
}
ops.push_back(Operand(param_type_id));
}
module_.PushType(spv::Op::OpTypeFunction, std::move(ops));
return func_type_id;
});
}
bool Builder::GenerateFunctionVariable(const ast::Variable* v) {
if (v->Is<ast::Const>()) {
// Constants are generated at their use. This is required as the 'const' declaration may be
// abstract-numeric, which has no SPIR-V type.
return true;
}
uint32_t init_id = 0;
if (v->initializer) {
init_id = GenerateExpression(v->initializer);
if (init_id == 0) {
return false;
}
}
auto* sem = builder_.Sem().Get(v);
if (v->Is<ast::Let>()) {
if (!v->initializer) {
TINT_ICE() << "missing initializer for let";
return false;
}
RegisterVariable(sem, init_id);
return true;
}
auto result = result_op();
auto var_id = std::get<uint32_t>(result);
auto sc = core::AddressSpace::kFunction;
auto* type = sem->Type();
auto type_id = GenerateTypeIfNeeded(type);
if (type_id == 0) {
return false;
}
module_.PushDebug(spv::Op::OpName, {Operand(var_id), Operand(v->name->symbol.Name())});
// TODO(dsinclair) We could detect if the initializer is fully const and emit
// an initializer value for the variable instead of doing the OpLoad.
auto null_id = GenerateConstantNullIfNeeded(type->UnwrapRef());
if (null_id == 0) {
return 0;
}
push_function_var(
{Operand(type_id), result, U32Operand(ConvertAddressSpace(sc)), Operand(null_id)});
if (v->initializer) {
if (!GenerateStore(var_id, init_id)) {
return false;
}
}
RegisterVariable(sem, var_id);
return true;
}
bool Builder::GenerateStore(uint32_t to, uint32_t from) {
return push_function_inst(spv::Op::OpStore, {Operand(to), Operand(from)});
}
bool Builder::GenerateGlobalVariable(const ast::Variable* v) {
if (v->Is<ast::Const>()) {
// Constants are generated at their use. This is required as the 'const' declaration may be
// abstract-numeric, which has no SPIR-V type.
return true;
}
auto* sem = builder_.Sem().Get<sem::GlobalVariable>(v);
if (TINT_UNLIKELY(!sem)) {
TINT_ICE() << "attempted to generate a global from a non-global variable";
return false;
}
auto* type = sem->Type()->UnwrapRef();
uint32_t init_id = 0;
if (auto* ctor = v->initializer) {
init_id = GenerateConstructorExpression(v, ctor);
if (init_id == 0) {
return false;
}
}
auto result = result_op();
auto var_id = std::get<uint32_t>(result);
auto sc = sem->AddressSpace() == core::AddressSpace::kUndefined ? core::AddressSpace::kPrivate
: sem->AddressSpace();
auto type_id = GenerateTypeIfNeeded(sem->Type());
if (type_id == 0) {
return false;
}
module_.PushDebug(spv::Op::OpName, {Operand(var_id), Operand(v->name->symbol.Name())});
OperandList ops = {Operand(type_id), result, U32Operand(ConvertAddressSpace(sc))};
if (v->initializer) {
ops.push_back(Operand(init_id));
} else {
auto* st = type->As<core::type::StorageTexture>();
if (st || type->Is<core::type::Struct>()) {
// type is a type::Struct or a type::StorageTexture
auto access = st ? st->access() : sem->Access();
switch (access) {
case core::Access::kWrite:
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationNonReadable)});
break;
case core::Access::kRead:
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationNonWritable)});
break;
case core::Access::kUndefined:
case core::Access::kReadWrite:
break;
}
}
if (!type->Is<core::type::Sampler>()) {
// If we don't have a initializer and we're an Output or Private
// variable, then WGSL requires that we zero-initialize.
// If we're a Workgroup variable, and the
// VK_KHR_zero_initialize_workgroup_memory extension is enabled, we should
// also zero-initialize.
if (sem->AddressSpace() == core::AddressSpace::kPrivate ||
sem->AddressSpace() == core::AddressSpace::kOut ||
(zero_initialize_workgroup_memory_ &&
sem->AddressSpace() == core::AddressSpace::kWorkgroup)) {
init_id = GenerateConstantNullIfNeeded(type);
if (init_id == 0) {
return 0;
}
ops.push_back(Operand(init_id));
}
}
}
module_.PushType(spv::Op::OpVariable, std::move(ops));
for (auto* attr : v->attributes) {
bool ok = Switch(
attr,
[&](const ast::BuiltinAttribute* builtin_attr) {
auto builtin = builder_.Sem().Get(builtin_attr)->Value();
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationBuiltIn),
U32Operand(ConvertBuiltin(builtin, sem->AddressSpace()))});
return true;
},
[&](const ast::LocationAttribute*) {
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationLocation),
Operand(sem->Attributes().location.value())});
return true;
},
[&](const ast::IndexAttribute*) {
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationIndex),
Operand(sem->Attributes().index.value())});
return true;
},
[&](const ast::InterpolateAttribute* interpolate) {
auto& s = builder_.Sem();
auto i_type =
s.Get<sem::BuiltinEnumExpression<core::InterpolationType>>(interpolate->type)
->Value();
auto i_smpl = core::InterpolationSampling::kUndefined;
if (interpolate->sampling) {
i_smpl = s.Get<sem::BuiltinEnumExpression<core::InterpolationSampling>>(
interpolate->sampling)
->Value();
}
AddInterpolationDecorations(var_id, i_type, i_smpl);
return true;
},
[&](const ast::InvariantAttribute*) {
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationInvariant)});
return true;
},
[&](const ast::BindingAttribute*) {
auto bp = sem->Attributes().binding_point;
module_.PushAnnot(
spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationBinding), Operand(bp->binding)});
return true;
},
[&](const ast::GroupAttribute*) {
auto bp = sem->Attributes().binding_point;
module_.PushAnnot(
spv::Op::OpDecorate,
{Operand(var_id), U32Operand(SpvDecorationDescriptorSet), Operand(bp->group)});
return true;
},
[&](const ast::IdAttribute*) {
return true; // Spec constants are handled elsewhere
},
[&](const ast::InternalAttribute*) {
return true; // ignored
}, //
TINT_ICE_ON_NO_MATCH);
if (!ok) {
return false;
}
}
RegisterVariable(sem, var_id);
return true;
}
bool Builder::GenerateIndexAccessor(const ast::IndexAccessorExpression* expr, AccessorInfo* info) {
auto idx_id = GenerateExpression(expr->index);
if (idx_id == 0) {
return 0;
}
// If the source is a reference, we access chain into it.
// In the future, pointers may support access-chaining.
// See https://github.com/gpuweb/gpuweb/pull/1580
if (info->source_type->Is<core::type::Reference>()) {
info->access_chain_indices.push_back(idx_id);
info->source_type = builder_.Sem().Get(expr)->UnwrapLoad()->Type();
return true;
}
auto result_type_id = GenerateTypeIfNeeded(TypeOf(expr));
if (result_type_id == 0) {
return false;
}
// We don't have a pointer, so we can just directly extract the value.
auto extract = result_op();
auto extract_id = std::get<uint32_t>(extract);
// If the index is compile-time constant, we use OpCompositeExtract.
auto* idx = builder_.Sem().GetVal(expr->index);
if (auto idx_constval = idx->ConstantValue()) {
if (!push_function_inst(spv::Op::OpCompositeExtract,
{
Operand(result_type_id),
extract,
Operand(info->source_id),
Operand(idx_constval->ValueAs<u32>()),
})) {
return false;
}
info->source_id = extract_id;
info->source_type = TypeOf(expr);
return true;
}
// If the source is a vector, we use OpVectorExtractDynamic.
if (TINT_LIKELY(info->source_type->Is<core::type::Vector>())) {
if (!push_function_inst(
spv::Op::OpVectorExtractDynamic,
{Operand(result_type_id), extract, Operand(info->source_id), Operand(idx_id)})) {
return false;
}
info->source_id = extract_id;
info->source_type = TypeOf(expr);
return true;
}
TINT_ICE() << "unsupported index accessor expression";
return false;
}
bool Builder::GenerateMemberAccessor(const ast::MemberAccessorExpression* expr,
AccessorInfo* info) {
auto* expr_sem = builder_.Sem().Get(expr)->UnwrapLoad();
auto* expr_type = expr_sem->Type();
return Switch(
expr_sem, //
[&](const sem::StructMemberAccess* access) {
uint32_t idx = access->Member()->Index();
if (info->source_type->Is<core::type::Reference>()) {
auto idx_id = GenerateConstantIfNeeded(ScalarConstant::U32(idx));
if (TINT_UNLIKELY(idx_id == 0)) {
return false;
}
info->access_chain_indices.push_back(idx_id);
info->source_type = expr_type;
} else {
auto result_type_id = GenerateTypeIfNeeded(expr_type);
if (TINT_UNLIKELY(result_type_id == 0)) {
return false;
}
auto extract = result_op();
auto extract_id = std::get<uint32_t>(extract);
if (!push_function_inst(spv::Op::OpCompositeExtract,
{Operand(result_type_id), extract, Operand(info->source_id),
Operand(idx)})) {
return false;
}
info->source_id = extract_id;
info->source_type = expr_type;
}
return true;
},
[&](const sem::Swizzle* swizzle) {
// Single element swizzle is either an access chain or a composite extract
auto& indices = swizzle->Indices();
if (indices.Length() == 1) {
if (info->source_type->Is<core::type::Reference>()) {
auto idx_id = GenerateConstantIfNeeded(ScalarConstant::U32(indices[0]));
if (TINT_UNLIKELY(idx_id == 0)) {
return false;
}
info->access_chain_indices.push_back(idx_id);
} else {
auto result_type_id = GenerateTypeIfNeeded(expr_type);
if (TINT_UNLIKELY(result_type_id == 0)) {
return false;
}
auto extract = result_op();
auto extract_id = std::get<uint32_t>(extract);
if (!push_function_inst(spv::Op::OpCompositeExtract,
{Operand(result_type_id), extract,
Operand(info->source_id), Operand(indices[0])})) {
return false;
}
info->source_id = extract_id;
info->source_type = expr_type;
}
return true;
}
// Store the type away as it may change if we run the access chain
auto* incoming_type = info->source_type;
// Multi-item extract is a VectorShuffle. We have to emit any existing
// access chain data, then load the access chain and shuffle that.
if (!info->access_chain_indices.empty()) {
auto result_type_id = GenerateTypeIfNeeded(info->source_type);
if (TINT_UNLIKELY(result_type_id == 0)) {
return false;
}
auto extract = result_op();
auto extract_id = std::get<uint32_t>(extract);
OperandList ops = {Operand(result_type_id), extract, Operand(info->source_id)};
for (auto id : info->access_chain_indices) {
ops.push_back(Operand(id));
}
if (!push_function_inst(spv::Op::OpAccessChain, ops)) {
return false;
}
info->source_id = GenerateLoadIfNeeded(expr_type, extract_id);
info->source_type = expr_type->UnwrapRef();
info->access_chain_indices.clear();
}
auto result_type_id = GenerateTypeIfNeeded(expr_type);
if (TINT_UNLIKELY(result_type_id == 0)) {
return false;
}
auto vec_id = GenerateLoadIfNeeded(incoming_type, info->source_id);
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
OperandList ops = {Operand(result_type_id), result, Operand(vec_id), Operand(vec_id)};
for (auto idx : indices) {
ops.push_back(Operand(idx));
}
if (!push_function_inst(spv::Op::OpVectorShuffle, ops)) {
return false;
}
info->source_id = result_id;
info->source_type = expr_type;
return true;
}, //
TINT_ICE_ON_NO_MATCH);
}
uint32_t Builder::GenerateAccessorExpression(const ast::AccessorExpression* expr) {
// Gather a list of all the member and index accessors that are in this chain.
// The list is built in reverse order as that's the order we need to access
// the chain.
std::vector<const ast::Expression*> accessors;
const ast::Expression* source = expr;
while (true) {
if (auto* array = source->As<ast::IndexAccessorExpression>()) {
accessors.insert(accessors.begin(), source);
source = array->object;
} else if (auto* member = source->As<ast::MemberAccessorExpression>()) {
accessors.insert(accessors.begin(), source);
source = member->object;
} else {
break;
}
// Stop traversing if we've hit a constant source expression.
if (builder_.Sem().GetVal(source)->ConstantValue()) {
break;
}
}
AccessorInfo info;
info.source_id = GenerateExpression(source);
if (info.source_id == 0) {
return 0;
}
info.source_type = TypeOf(source);
// Note: Dynamic index on array and matrix values (lets) should have been
// promoted to storage with the VarForDynamicIndex transform.
for (auto* accessor : accessors) {
bool ok = Switch(
accessor,
[&](const ast::IndexAccessorExpression* array) {
return GenerateIndexAccessor(array, &info);
},
[&](const ast::MemberAccessorExpression* member) {
return GenerateMemberAccessor(member, &info);
}, //
TINT_ICE_ON_NO_MATCH);
if (!ok) {
return false;
}
}
if (!info.access_chain_indices.empty()) {
auto* type = builder_.Sem().Get(expr)->UnwrapLoad()->Type();
auto result_type_id = GenerateTypeIfNeeded(type);
if (result_type_id == 0) {
return 0;
}
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
OperandList ops = {Operand(result_type_id), result, Operand(info.source_id)};
for (auto id : info.access_chain_indices) {
ops.push_back(Operand(id));
}
if (!push_function_inst(spv::Op::OpAccessChain, ops)) {
return false;
}
info.source_id = result_id;
}
return info.source_id;
}
uint32_t Builder::GenerateIdentifierExpression(const ast::IdentifierExpression* expr) {
if (auto* sem = builder_.Sem().GetVal(expr); sem) {
if (auto* user = sem->UnwrapLoad()->As<sem::VariableUser>()) {
return LookupVariableID(user->Variable());
}
}
TINT_ICE() << "identifier '" + expr->identifier->symbol.Name() +
"' does not resolve to a variable";
return 0;
}
uint32_t Builder::GenerateLoad(const core::type::Reference* type, uint32_t id) {
auto type_id = GenerateTypeIfNeeded(type->StoreType());
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
if (!push_function_inst(spv::Op::OpLoad, {Operand(type_id), result, Operand(id)})) {
return 0;
}
return result_id;
}
uint32_t Builder::GenerateLoadIfNeeded(const core::type::Type* type, uint32_t id) {
if (auto* ref = type->As<core::type::Reference>()) {
return GenerateLoad(ref, id);
}
return id;
}
uint32_t Builder::GenerateUnaryOpExpression(const ast::UnaryOpExpression* expr) {
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
spv::Op op = spv::Op::OpNop;
switch (expr->op) {
case core::UnaryOp::kComplement:
op = spv::Op::OpNot;
break;
case core::UnaryOp::kNegation:
if (TypeOf(expr)->is_float_scalar_or_vector()) {
op = spv::Op::OpFNegate;
} else {
op = spv::Op::OpSNegate;
}
break;
case core::UnaryOp::kNot:
op = spv::Op::OpLogicalNot;
break;
case core::UnaryOp::kAddressOf:
case core::UnaryOp::kIndirection:
// Address-of converts a reference to a pointer, and dereference converts
// a pointer to a reference. These are the same thing in SPIR-V, so this
// is a no-op.
return GenerateExpression(expr->expr);
}
auto val_id = GenerateExpression(expr->expr);
if (val_id == 0) {
return 0;
}
auto type_id = GenerateTypeIfNeeded(TypeOf(expr));
if (type_id == 0) {
return 0;
}
if (!push_function_inst(op, {Operand(type_id), result, Operand(val_id)})) {
return false;
}
return result_id;
}
uint32_t Builder::GetGLSLstd450Import() {
auto where = import_name_to_id_.find(kGLSLstd450);
if (where != import_name_to_id_.end()) {
return where->second;
}
// It doesn't exist yet. Generate it.
auto result = result_op();
auto id = std::get<uint32_t>(result);
module_.PushExtImport(spv::Op::OpExtInstImport, {result, Operand(kGLSLstd450)});
// Remember it for later.
import_name_to_id_[kGLSLstd450] = id;
return id;
}
uint32_t Builder::GenerateConstructorExpression(const ast::Variable* var,
const ast::Expression* expr) {
if (auto* sem = builder_.Sem().GetVal(expr)) {
if (auto constant = sem->ConstantValue()) {
return GenerateConstantIfNeeded(constant);
}
}
if (auto* call = builder_.Sem().Get<sem::Call>(expr)) {
if (call->Target()->IsAnyOf<sem::ValueConstructor, sem::ValueConversion>()) {
return GenerateValueConstructorOrConversion(call, var);
}
}
TINT_ICE() << "unknown constructor expression";
return 0;
}
bool Builder::IsConstructorConst(const ast::CallExpression* expr) {
bool is_const = true;
ast::TraverseExpressions(expr, [&](const ast::Expression* e) {
auto* val = builder_.Sem().GetVal(e);
if (!val) {
return ast::TraverseAction::Descend;
}
if (e->Is<ast::LiteralExpression>()) {
return ast::TraverseAction::Descend;
}
if (e->Is<ast::CallExpression>()) {
if (val->Is<sem::Materialize>()) {
// Materialize can only occur on compile time expressions, so this sub-tree must be
// constant.
return ast::TraverseAction::Skip;
}
auto* call = val->As<sem::Call>();
if (call->Target()->Is<sem::ValueConstructor>()) {
return ast::TraverseAction::Descend;
}
}
is_const = false;
return ast::TraverseAction::Stop;
});
return is_const;
}
uint32_t Builder::GenerateValueConstructorOrConversion(const sem::Call* call,
const ast::Variable* var) {
auto& args = call->Arguments();
auto* global_var = builder_.Sem().Get<sem::GlobalVariable>(var);
auto* result_type = call->Type();
// Generate the zero constructor if there are no values provided.
if (args.IsEmpty()) {
return GenerateConstantNullIfNeeded(result_type->UnwrapRef());
}
result_type = result_type->UnwrapRef();
bool constructor_is_const = IsConstructorConst(call->Declaration());
if (has_error()) {
return 0;
}
bool can_cast_or_copy = result_type->Is<core::type::Scalar>();
if (auto* res_vec = result_type->As<core::type::Vector>()) {
if (res_vec->type()->Is<core::type::Scalar>()) {
auto* value_type = args[0]->Type()->UnwrapRef();
if (auto* val_vec = value_type->As<core::type::Vector>()) {
if (val_vec->type()->Is<core::type::Scalar>()) {
can_cast_or_copy = res_vec->Width() == val_vec->Width();
}
}
}
}
if (auto* res_mat = result_type->As<core::type::Matrix>()) {
auto* value_type = args[0]->Type()->UnwrapRef();
if (auto* val_mat = value_type->As<core::type::Matrix>()) {
// Generate passthrough for matrices of the same type
can_cast_or_copy = res_mat == val_mat;
}
}
if (can_cast_or_copy) {
return GenerateCastOrCopyOrPassthrough(result_type, args[0]->Declaration(), global_var);
}
auto type_id = GenerateTypeIfNeeded(result_type);
if (type_id == 0) {
return 0;
}
bool result_is_constant_composite = constructor_is_const;
bool result_is_spec_composite = false;
if (auto* vec = result_type->As<core::type::Vector>()) {
result_type = vec->type();
}
OperandList ops;
static constexpr size_t kOpsResultIdx = 1;
static constexpr size_t kOpsFirstValueIdx = 2;
ops.reserve(8);
ops.push_back(Operand(type_id));
ops.push_back(Operand(0u)); // Placeholder for the result ID
for (auto* e : args) {
uint32_t id = 0;
id = GenerateExpression(e);
if (id == 0) {
return 0;
}
auto* value_type = e->Type()->UnwrapRef();
// If the result and value types are the same we can just use the object.
// If the result is not a vector then we should have validated that the
// value type is a correctly sized vector so we can just use it directly.
if (result_type == value_type || result_type->Is<core::type::Matrix>() ||
result_type->Is<core::type::Array>() || result_type->Is<core::type::Struct>()) {
ops.push_back(Operand(id));
continue;
}
// Both scalars, but not the same type so we need to generate a conversion
// of the value.
if (value_type->Is<core::type::Scalar>() && result_type->Is<core::type::Scalar>()) {
id = GenerateCastOrCopyOrPassthrough(result_type, args[0]->Declaration(), global_var);
ops.push_back(Operand(id));
continue;
}
// When handling vectors as the values there a few cases to take into
// consideration:
// 1. Module scoped vec3<f32>(vec2<f32>(1, 2), 3) -> OpSpecConstantOp
// 2. Function scoped vec3<f32>(vec2<f32>(1, 2), 3) -> OpCompositeExtract
// 3. Either array<vec3<f32>, 1>(vec3<f32>(1, 2, 3)) -> use the ID.
// -> handled above
//
// For cases 1 and 2, if the type is different we also may need to insert
// a type cast.
if (auto* vec = value_type->As<core::type::Vector>()) {
auto* vec_type = vec->type();
auto value_type_id = GenerateTypeIfNeeded(vec_type);
if (value_type_id == 0) {
return 0;
}
for (uint32_t i = 0; i < vec->Width(); ++i) {
auto extract = result_op();
auto extract_id = std::get<uint32_t>(extract);
if (!global_var) {
// A non-global initializer. Case 2.
if (!push_function_inst(
spv::Op::OpCompositeExtract,
{Operand(value_type_id), extract, Operand(id), Operand(i)})) {
return false;
}
// We no longer have a constant composite, but have to do a
// composite construction as these calls are inside a function.
result_is_constant_composite = false;
} else {
// A global initializer, must use OpSpecConstantOp. Case 1.
auto idx_id = GenerateConstantIfNeeded(ScalarConstant::U32(i));
if (idx_id == 0) {
return 0;
}
module_.PushType(spv::Op::OpSpecConstantOp, {Operand(value_type_id), extract,
U32Operand(SpvOpCompositeExtract),
Operand(id), Operand(idx_id)});
result_is_spec_composite = true;
}
ops.push_back(Operand(extract_id));
}
} else {
TINT_ICE() << "Unhandled type cast value type";
return 0;
}
}
// For a single-value vector initializer, splat the initializer value.
auto* const init_result_type = call->Type()->UnwrapRef();
if (args.Length() == 1 && init_result_type->is_scalar_vector() &&
args[0]->Type()->UnwrapRef()->Is<core::type::Scalar>()) {
size_t vec_size = init_result_type->As<core::type::Vector>()->Width();
for (size_t i = 0; i < (vec_size - 1); ++i) {
ops.push_back(ops[kOpsFirstValueIdx]);
}
}
auto& stack = (result_is_spec_composite || result_is_constant_composite)
? scope_stack_[0] // Global scope
: scope_stack_.back(); // Lexical scope
return tint::GetOrCreate(stack.type_init_to_id_, OperandListKey{ops}, [&]() -> uint32_t {
auto result = result_op();
ops[kOpsResultIdx] = result;
if (result_is_spec_composite) {
module_.PushType(spv::Op::OpSpecConstantComposite, ops);
} else if (result_is_constant_composite) {
module_.PushType(spv::Op::OpConstantComposite, ops);
} else {
if (!push_function_inst(spv::Op::OpCompositeConstruct, ops)) {
return 0;
}
}
return std::get<uint32_t>(result);
});
}
uint32_t Builder::GenerateCastOrCopyOrPassthrough(const core::type::Type* to_type,
const ast::Expression* from_expr,
bool is_global_init) {
// This should not happen as we rely on constant folding to obviate
// casts/conversions for module-scope variables
if (TINT_UNLIKELY(is_global_init)) {
TINT_ICE() << "Module-level conversions are not supported. Conversions should "
"have already been constant-folded by the FoldConstants transform.";
return 0;
}
auto elem_type_of = [](const core::type::Type* t) -> const core::type::Type* {
if (t->Is<core::type::Scalar>()) {
return t;
}
if (auto* v = t->As<core::type::Vector>()) {
return v->type();
}
return nullptr;
};
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
auto result_type_id = GenerateTypeIfNeeded(to_type);
if (result_type_id == 0) {
return 0;
}
auto val_id = GenerateExpression(from_expr);
if (val_id == 0) {
return 0;
}
auto* from_type = TypeOf(from_expr)->UnwrapRef();
spv::Op op = spv::Op::OpNop;
if ((from_type->Is<core::type::I32>() && to_type->is_float_scalar()) ||
(from_type->is_signed_integer_vector() && to_type->is_float_vector())) {
op = spv::Op::OpConvertSToF;
} else if ((from_type->Is<core::type::U32>() && to_type->is_float_scalar()) ||
(from_type->is_unsigned_integer_vector() && to_type->is_float_vector())) {
op = spv::Op::OpConvertUToF;
} else if ((from_type->is_float_scalar() && to_type->Is<core::type::I32>()) ||
(from_type->is_float_vector() && to_type->is_signed_integer_vector())) {
op = spv::Op::OpConvertFToS;
} else if ((from_type->is_float_scalar() && to_type->Is<core::type::U32>()) ||
(from_type->is_float_vector() && to_type->is_unsigned_integer_vector())) {
op = spv::Op::OpConvertFToU;
} else if (from_type->IsAnyOf<core::type::Bool, core::type::F32, core::type::I32,
core::type::U32, core::type::F16, core::type::Vector>() &&
from_type == to_type) {
// Identity initializer for scalar and vector types
return val_id;
} else if ((from_type->is_float_scalar() && to_type->is_float_scalar()) ||
(from_type->is_float_vector() && to_type->is_float_vector() &&
from_type->As<core::type::Vector>()->Width() ==
to_type->As<core::type::Vector>()->Width())) {
// Convert between f32 and f16 types.
// OpFConvert requires the scalar component types to be different, and the case of from_type
// and to_type being the same floating point scalar or vector type, i.e. identity
// initializer, is already handled in the previous else-if clause.
op = spv::Op::OpFConvert;
} else if ((from_type->Is<core::type::I32>() && to_type->Is<core::type::U32>()) ||
(from_type->Is<core::type::U32>() && to_type->Is<core::type::I32>()) ||
(from_type->is_signed_integer_vector() && to_type->is_unsigned_integer_vector()) ||
(from_type->is_unsigned_integer_vector() &&
to_type->is_integer_scalar_or_vector())) {
op = spv::Op::OpBitcast;
} else if ((from_type->Is<core::type::NumericScalar>() && to_type->Is<core::type::Bool>()) ||
(from_type->is_numeric_vector() && to_type->is_bool_vector())) {
// Convert scalar (vector) to bool (vector)
// Return the result of comparing from_expr with zero
uint32_t zero = GenerateConstantNullIfNeeded(from_type);
const auto* from_elem_type = elem_type_of(from_type);
op = from_elem_type->is_integer_scalar() ? spv::Op::OpINotEqual : spv::Op::OpFUnordNotEqual;
if (!push_function_inst(op, {Operand(result_type_id), Operand(result_id), Operand(val_id),
Operand(zero)})) {
return 0;
}
return result_id;
} else if (from_type->is_bool_scalar_or_vector() && to_type->is_numeric_scalar_or_vector()) {
// Convert bool scalar/vector to numeric scalar/vector.
// Use the bool to select between 1 (if true) and 0 (if false).
const auto* to_elem_type = elem_type_of(to_type);
uint32_t one_id;
uint32_t zero_id;
if (to_elem_type->Is<core::type::F32>()) {
zero_id = GenerateConstantIfNeeded(ScalarConstant::F32(0));
one_id = GenerateConstantIfNeeded(ScalarConstant::F32(1));
} else if (to_elem_type->Is<core::type::F16>()) {
zero_id = GenerateConstantIfNeeded(ScalarConstant::F16(0));
one_id = GenerateConstantIfNeeded(ScalarConstant::F16(1));
} else if (to_elem_type->Is<core::type::U32>()) {
zero_id = GenerateConstantIfNeeded(ScalarConstant::U32(0));
one_id = GenerateConstantIfNeeded(ScalarConstant::U32(1));
} else if (to_elem_type->Is<core::type::I32>()) {
zero_id = GenerateConstantIfNeeded(ScalarConstant::I32(0));
one_id = GenerateConstantIfNeeded(ScalarConstant::I32(1));
} else {
TINT_ICE() << "invalid destination type for bool conversion";
return false;
}
if (auto* to_vec = to_type->As<core::type::Vector>()) {
// Splat the scalars into vectors.
zero_id = GenerateConstantVectorSplatIfNeeded(to_vec, zero_id);
one_id = GenerateConstantVectorSplatIfNeeded(to_vec, one_id);
}
if (!one_id || !zero_id) {
return false;
}
op = spv::Op::OpSelect;
if (!push_function_inst(op, {Operand(result_type_id), Operand(result_id), Operand(val_id),
Operand(one_id), Operand(zero_id)})) {
return 0;
}
return result_id;
} else if (TINT_LIKELY(from_type->Is<core::type::Matrix>() &&
to_type->Is<core::type::Matrix>())) {
// SPIRV does not support matrix conversion, the only valid case is matrix identity
// initializer. Matrix conversion between f32 and f16 should be transformed into vector
// conversions for each column vectors by VectorizeMatrixConversions.
auto* from_mat = from_type->As<core::type::Matrix>();
auto* to_mat = to_type->As<core::type::Matrix>();
if (TINT_LIKELY(from_mat == to_mat)) {
return val_id;
}
TINT_ICE() << "matrix conversion is not supported and should have been handled by "
"VectorizeMatrixConversions";
} else {
TINT_ICE() << "Invalid from_type";
}
if (op == spv::Op::OpNop) {
TINT_ICE() << "unable to determine conversion type for cast, from: " +
from_type->FriendlyName() + " to: " + to_type->FriendlyName();
return 0;
}
if (!push_function_inst(op, {Operand(result_type_id), result, Operand(val_id)})) {
return 0;
}
return result_id;
}
uint32_t Builder::GenerateLiteralIfNeeded(const ast::LiteralExpression* lit) {
ScalarConstant constant;
Switch(
lit,
[&](const ast::BoolLiteralExpression* l) {
constant.kind = ScalarConstant::Kind::kBool;
constant.value.b = l->value;
},
[&](const ast::IntLiteralExpression* i) {
switch (i->suffix) {
case ast::IntLiteralExpression::Suffix::kNone:
case ast::IntLiteralExpression::Suffix::kI:
constant.kind = ScalarConstant::Kind::kI32;
constant.value.i32 = static_cast<int32_t>(i->value);
return;
case ast::IntLiteralExpression::Suffix::kU:
constant.kind = ScalarConstant::Kind::kU32;
constant.value.u32 = static_cast<uint32_t>(i->value);
return;
}
},
[&](const ast::FloatLiteralExpression* f) {
switch (f->suffix) {
case ast::FloatLiteralExpression::Suffix::kNone:
case ast::FloatLiteralExpression::Suffix::kF:
constant.kind = ScalarConstant::Kind::kF32;
constant.value.f32 = static_cast<float>(f->value);
return;
case ast::FloatLiteralExpression::Suffix::kH:
constant.kind = ScalarConstant::Kind::kF16;
constant.value.f16 = {f16(static_cast<float>(f->value)).BitsRepresentation()};
return;
}
}, //
TINT_ICE_ON_NO_MATCH);
if (has_error()) {
return false;
}
return GenerateConstantIfNeeded(constant);
}
uint32_t Builder::GenerateConstantIfNeeded(const core::constant::Value* constant) {
if (constant->AllZero()) {
return GenerateConstantNullIfNeeded(constant->Type());
}
auto* ty = constant->Type();
auto composite = [&](size_t el_count) -> uint32_t {
auto type_id = GenerateTypeIfNeeded(ty);
if (!type_id) {
return 0;
}
static constexpr size_t kOpsResultIdx = 1; // operand index of the result
std::vector<Operand> ops;
ops.reserve(el_count + 2);
ops.emplace_back(type_id);
ops.push_back(Operand(0u)); // Placeholder for the result ID
for (size_t i = 0; i < el_count; i++) {
auto id = GenerateConstantIfNeeded(constant->Index(i));
if (!id) {
return 0;
}
ops.emplace_back(id);
}
auto& global_scope = scope_stack_[0];
return tint::GetOrCreate(global_scope.type_init_to_id_, OperandListKey{ops},
[&]() -> uint32_t {
auto result = result_op();
ops[kOpsResultIdx] = result;
module_.PushType(spv::Op::OpConstantComposite, std::move(ops));
return std::get<uint32_t>(result);
});
};
return Switch(
ty, //
[&](const core::type::Bool*) {
bool val = constant->ValueAs<bool>();
return GenerateConstantIfNeeded(ScalarConstant::Bool(val));
},
[&](const core::type::F32*) {
auto val = constant->ValueAs<f32>();
return GenerateConstantIfNeeded(ScalarConstant::F32(val.value));
},
[&](const core::type::F16*) {
auto val = constant->ValueAs<f16>();
return GenerateConstantIfNeeded(ScalarConstant::F16(val.value));
},
[&](const core::type::I32*) {
auto val = constant->ValueAs<i32>();
return GenerateConstantIfNeeded(ScalarConstant::I32(val.value));
},
[&](const core::type::U32*) {
auto val = constant->ValueAs<u32>();
return GenerateConstantIfNeeded(ScalarConstant::U32(val.value));
},
[&](const core::type::Vector* v) { return composite(v->Width()); },
[&](const core::type::Matrix* m) { return composite(m->columns()); },
[&](const core::type::Array* a) {
auto count = a->ConstantCount();
if (!count) {
TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
return static_cast<uint32_t>(0);
}
return composite(count.value());
},
[&](const core::type::Struct* s) { return composite(s->Members().Length()); }, //
TINT_ICE_ON_NO_MATCH);
}
uint32_t Builder::GenerateConstantIfNeeded(const ScalarConstant& constant) {
auto it = const_to_id_.find(constant);
if (it != const_to_id_.end()) {
return it->second;
}
uint32_t type_id = 0;
switch (constant.kind) {
case ScalarConstant::Kind::kU32: {
type_id = GenerateTypeIfNeeded(builder_.create<core::type::U32>());
break;
}
case ScalarConstant::Kind::kI32: {
type_id = GenerateTypeIfNeeded(builder_.create<core::type::I32>());
break;
}
case ScalarConstant::Kind::kF32: {
type_id = GenerateTypeIfNeeded(builder_.create<core::type::F32>());
break;
}
case ScalarConstant::Kind::kF16: {
type_id = GenerateTypeIfNeeded(builder_.create<core::type::F16>());
break;
}
case ScalarConstant::Kind::kBool: {
type_id = GenerateTypeIfNeeded(builder_.create<core::type::Bool>());
break;
}
}
if (type_id == 0) {
return 0;
}
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
switch (constant.kind) {
case ScalarConstant::Kind::kU32: {
module_.PushType(spv::Op::OpConstant,
{Operand(type_id), result, Operand(constant.value.u32)});
break;
}
case ScalarConstant::Kind::kI32: {
module_.PushType(spv::Op::OpConstant,
{Operand(type_id), result, U32Operand(constant.value.i32)});
break;
}
case ScalarConstant::Kind::kF32: {
module_.PushType(spv::Op::OpConstant,
{Operand(type_id), result, Operand(constant.value.f32)});
break;
}
case ScalarConstant::Kind::kF16: {
module_.PushType(
spv::Op::OpConstant,
{Operand(type_id), result, U32Operand(constant.value.f16.bits_representation)});
break;
}
case ScalarConstant::Kind::kBool: {
if (constant.value.b) {
module_.PushType(spv::Op::OpConstantTrue, {Operand(type_id), result});
} else {
module_.PushType(spv::Op::OpConstantFalse, {Operand(type_id), result});
}
break;
}
}
const_to_id_[constant] = result_id;
return result_id;
}
uint32_t Builder::GenerateConstantNullIfNeeded(const core::type::Type* type) {
auto type_id = GenerateTypeIfNeeded(type);
if (type_id == 0) {
return 0;
}
return tint::GetOrCreate(const_null_to_id_, type, [&] {
auto result = result_op();
module_.PushType(spv::Op::OpConstantNull, {Operand(type_id), result});
return std::get<uint32_t>(result);
});
}
uint32_t Builder::GenerateConstantVectorSplatIfNeeded(const core::type::Vector* type,
uint32_t value_id) {
auto type_id = GenerateTypeIfNeeded(type);
if (type_id == 0 || value_id == 0) {
return 0;
}
uint64_t key = (static_cast<uint64_t>(type->Width()) << 32) + value_id;
return tint::GetOrCreate(const_splat_to_id_, key, [&] {
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
OperandList ops;
ops.push_back(Operand(type_id));
ops.push_back(result);
for (uint32_t i = 0; i < type->Width(); i++) {
ops.push_back(Operand(value_id));
}
module_.PushType(spv::Op::OpConstantComposite, ops);
const_splat_to_id_[key] = result_id;
return result_id;
});
}
uint32_t Builder::GenerateShortCircuitBinaryExpression(const ast::BinaryExpression* expr) {
auto lhs_id = GenerateExpression(expr->lhs);
if (lhs_id == 0) {
return false;
}
// Get the ID of the basic block where control flow will diverge. It's the
// last basic block generated for the left-hand-side of the operator.
auto original_label_id = current_label_id_;
auto type_id = GenerateTypeIfNeeded(TypeOf(expr));
if (type_id == 0) {
return 0;
}
auto merge_block = result_op();
auto merge_block_id = std::get<uint32_t>(merge_block);
auto block = result_op();
auto block_id = std::get<uint32_t>(block);
auto true_block_id = block_id;
auto false_block_id = merge_block_id;
// For a logical or we want to only check the RHS if the LHS is failed.
if (expr->IsLogicalOr()) {
std::swap(true_block_id, false_block_id);
}
if (!push_function_inst(spv::Op::OpSelectionMerge,
{Operand(merge_block_id), U32Operand(SpvSelectionControlMaskNone)})) {
return 0;
}
if (!push_function_inst(spv::Op::OpBranchConditional,
{Operand(lhs_id), Operand(true_block_id), Operand(false_block_id)})) {
return 0;
}
// Output block to check the RHS
if (!GenerateLabel(block_id)) {
return 0;
}
auto rhs_id = GenerateExpression(expr->rhs);
if (rhs_id == 0) {
return 0;
}
// Get the block ID of the last basic block generated for the right-hand-side
// expression. That block will be an immediate predecessor to the merge block.
auto rhs_block_id = current_label_id_;
if (!push_function_inst(spv::Op::OpBranch, {Operand(merge_block_id)})) {
return 0;
}
// Output the merge block
if (!GenerateLabel(merge_block_id)) {
return 0;
}
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
if (!push_function_inst(spv::Op::OpPhi,
{Operand(type_id), result, Operand(lhs_id), Operand(original_label_id),
Operand(rhs_id), Operand(rhs_block_id)})) {
return 0;
}
return result_id;
}
uint32_t Builder::GenerateSplat(uint32_t scalar_id, const core::type::Type* vec_type) {
// Create a new vector to splat scalar into
auto splat_vector = result_op();
auto* splat_vector_type = builder_.create<core::type::Pointer>(
core::AddressSpace::kFunction, vec_type, core::Access::kReadWrite);
push_function_var({Operand(GenerateTypeIfNeeded(splat_vector_type)), splat_vector,
U32Operand(ConvertAddressSpace(core::AddressSpace::kFunction)),
Operand(GenerateConstantNullIfNeeded(vec_type))});
// Splat scalar into vector
auto splat_result = result_op();
OperandList ops;
ops.push_back(Operand(GenerateTypeIfNeeded(vec_type)));
ops.push_back(splat_result);
for (size_t i = 0; i < vec_type->As<core::type::Vector>()->Width(); ++i) {
ops.push_back(Operand(scalar_id));
}
if (!push_function_inst(spv::Op::OpCompositeConstruct, ops)) {
return 0;
}
return std::get<uint32_t>(splat_result);
}
uint32_t Builder::GenerateMatrixAddOrSub(uint32_t lhs_id,
uint32_t rhs_id,
const core::type::Matrix* type,
spv::Op op) {
// Example addition of two matrices:
// %31 = OpLoad %mat3v4float %m34
// %32 = OpLoad %mat3v4float %m34
// %33 = OpCompositeExtract %v4float %31 0
// %34 = OpCompositeExtract %v4float %32 0
// %35 = OpFAdd %v4float %33 %34
// %36 = OpCompositeExtract %v4float %31 1
// %37 = OpCompositeExtract %v4float %32 1
// %38 = OpFAdd %v4float %36 %37
// %39 = OpCompositeExtract %v4float %31 2
// %40 = OpCompositeExtract %v4float %32 2
// %41 = OpFAdd %v4float %39 %40
// %42 = OpCompositeConstruct %mat3v4float %35 %38 %41
auto* column_type = builder_.create<core::type::Vector>(type->type(), type->rows());
auto column_type_id = GenerateTypeIfNeeded(column_type);
OperandList ops;
for (uint32_t i = 0; i < type->columns(); ++i) {
// Extract column `i` from lhs mat
auto lhs_column_id = result_op();
if (!push_function_inst(
spv::Op::OpCompositeExtract,
{Operand(column_type_id), lhs_column_id, Operand(lhs_id), Operand(i)})) {
return 0;
}
// Extract column `i` from rhs mat
auto rhs_column_id = result_op();
if (!push_function_inst(
spv::Op::OpCompositeExtract,
{Operand(column_type_id), rhs_column_id, Operand(rhs_id), Operand(i)})) {
return 0;
}
// Add or subtract the two columns
auto result = result_op();
if (!push_function_inst(op,
{Operand(column_type_id), result, lhs_column_id, rhs_column_id})) {
return 0;
}
ops.push_back(result);
}
// Create the result matrix from the added/subtracted column vectors
auto result_mat_id = result_op();
ops.insert(ops.begin(), result_mat_id);
ops.insert(ops.begin(), Operand(GenerateTypeIfNeeded(type)));
if (!push_function_inst(spv::Op::OpCompositeConstruct, ops)) {
return 0;
}
return std::get<uint32_t>(result_mat_id);
}
uint32_t Builder::GenerateBinaryExpression(const ast::BinaryExpression* expr) {
// There is special logic for short circuiting operators.
if (expr->IsLogicalAnd() || expr->IsLogicalOr()) {
return GenerateShortCircuitBinaryExpression(expr);
}
auto lhs_id = GenerateExpression(expr->lhs);
if (lhs_id == 0) {
return 0;
}
auto rhs_id = GenerateExpression(expr->rhs);
if (rhs_id == 0) {
return 0;
}
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
auto type_id = GenerateTypeIfNeeded(TypeOf(expr));
if (type_id == 0) {
return 0;
}
// Handle int and float and the vectors of those types. Other types
// should have been rejected by validation.
auto* lhs_type = TypeOf(expr->lhs)->UnwrapRef();
auto* rhs_type = TypeOf(expr->rhs)->UnwrapRef();
// Handle matrix-matrix addition and subtraction
if ((expr->IsAdd() || expr->IsSubtract()) && lhs_type->is_float_matrix() &&
rhs_type->is_float_matrix()) {
auto* lhs_mat = lhs_type->As<core::type::Matrix>();
auto* rhs_mat = rhs_type->As<core::type::Matrix>();
// This should already have been validated by resolver
if (lhs_mat->rows() != rhs_mat->rows() || lhs_mat->columns() != rhs_mat->columns()) {
TINT_ICE() << "matrices must have same dimensionality for add or subtract";
return 0;
}
return GenerateMatrixAddOrSub(lhs_id, rhs_id, lhs_mat,
expr->IsAdd() ? spv::Op::OpFAdd : spv::Op::OpFSub);
}
// For vector-scalar arithmetic operations, splat scalar into a vector. We
// skip this for multiply as we can use OpVectorTimesScalar.
const bool is_float_scalar_vector_multiply =
expr->IsMultiply() && ((lhs_type->is_float_scalar() && rhs_type->is_float_vector()) ||
(lhs_type->is_float_vector() && rhs_type->is_float_scalar()));
if (expr->IsArithmetic() && !is_float_scalar_vector_multiply) {
if (lhs_type->Is<core::type::Vector>() && rhs_type->Is<core::type::NumericScalar>()) {
uint32_t splat_vector_id = GenerateSplat(rhs_id, lhs_type);
if (splat_vector_id == 0) {
return 0;
}
rhs_id = splat_vector_id;
rhs_type = lhs_type;
} else if (lhs_type->Is<core::type::NumericScalar>() &&
rhs_type->Is<core::type::Vector>()) {
uint32_t splat_vector_id = GenerateSplat(lhs_id, rhs_type);
if (splat_vector_id == 0) {
return 0;
}
lhs_id = splat_vector_id;
lhs_type = rhs_type;
}
}
bool lhs_is_float_or_vec = lhs_type->is_float_scalar_or_vector();
bool lhs_is_bool_or_vec = lhs_type->is_bool_scalar_or_vector();
bool lhs_is_integer_or_vec = lhs_type->is_integer_scalar_or_vector();
bool lhs_is_unsigned = lhs_type->is_unsigned_integer_scalar_or_vector();
spv::Op op = spv::Op::OpNop;
if (expr->IsAnd()) {
if (lhs_is_integer_or_vec) {
op = spv::Op::OpBitwiseAnd;
} else if (lhs_is_bool_or_vec) {
op = spv::Op::OpLogicalAnd;
} else {
TINT_ICE() << "invalid and expression";
return 0;
}
} else if (expr->IsAdd()) {
op = lhs_is_float_or_vec ? spv::Op::OpFAdd : spv::Op::OpIAdd;
} else if (expr->IsDivide()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFDiv;
} else if (lhs_is_unsigned) {
op = spv::Op::OpUDiv;
} else {
op = spv::Op::OpSDiv;
}
} else if (expr->IsEqual()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFOrdEqual;
} else if (lhs_is_bool_or_vec) {
op = spv::Op::OpLogicalEqual;
} else if (lhs_is_integer_or_vec) {
op = spv::Op::OpIEqual;
} else {
TINT_ICE() << "invalid equal expression";
return 0;
}
} else if (expr->IsGreaterThan()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFOrdGreaterThan;
} else if (lhs_is_unsigned) {
op = spv::Op::OpUGreaterThan;
} else {
op = spv::Op::OpSGreaterThan;
}
} else if (expr->IsGreaterThanEqual()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFOrdGreaterThanEqual;
} else if (lhs_is_unsigned) {
op = spv::Op::OpUGreaterThanEqual;
} else {
op = spv::Op::OpSGreaterThanEqual;
}
} else if (expr->IsLessThan()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFOrdLessThan;
} else if (lhs_is_unsigned) {
op = spv::Op::OpULessThan;
} else {
op = spv::Op::OpSLessThan;
}
} else if (expr->IsLessThanEqual()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFOrdLessThanEqual;
} else if (lhs_is_unsigned) {
op = spv::Op::OpULessThanEqual;
} else {
op = spv::Op::OpSLessThanEqual;
}
} else if (expr->IsModulo()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFRem;
} else if (lhs_is_unsigned) {
op = spv::Op::OpUMod;
} else {
op = spv::Op::OpSRem;
}
} else if (expr->IsMultiply()) {
if (lhs_type->is_integer_scalar_or_vector()) {
// If the left hand side is an integer then this _has_ to be OpIMul as
// there there is no other integer multiplication.
op = spv::Op::OpIMul;
} else if (lhs_type->is_float_scalar() && rhs_type->is_float_scalar()) {
// Float scalars multiply with OpFMul
op = spv::Op::OpFMul;
} else if (lhs_type->is_float_vector() && rhs_type->is_float_vector()) {
// Float vectors must be validated to be the same size and then use OpFMul
op = spv::Op::OpFMul;
} else if (lhs_type->is_float_scalar() && rhs_type->is_float_vector()) {
// Scalar * Vector we need to flip lhs and rhs types
// because OpVectorTimesScalar expects <vector>, <scalar>
std::swap(lhs_id, rhs_id);
op = spv::Op::OpVectorTimesScalar;
} else if (lhs_type->is_float_vector() && rhs_type->is_float_scalar()) {
// float vector * scalar
op = spv::Op::OpVectorTimesScalar;
} else if (lhs_type->is_float_scalar() && rhs_type->is_float_matrix()) {
// Scalar * Matrix we need to flip lhs and rhs types because
// OpMatrixTimesScalar expects <matrix>, <scalar>
std::swap(lhs_id, rhs_id);
op = spv::Op::OpMatrixTimesScalar;
} else if (lhs_type->is_float_matrix() && rhs_type->is_float_scalar()) {
// float matrix * scalar
op = spv::Op::OpMatrixTimesScalar;
} else if (lhs_type->is_float_vector() && rhs_type->is_float_matrix()) {
// float vector * matrix
op = spv::Op::OpVectorTimesMatrix;
} else if (lhs_type->is_float_matrix() && rhs_type->is_float_vector()) {
// float matrix * vector
op = spv::Op::OpMatrixTimesVector;
} else if (lhs_type->is_float_matrix() && rhs_type->is_float_matrix()) {
// float matrix * matrix
op = spv::Op::OpMatrixTimesMatrix;
} else {
TINT_ICE() << "invalid multiply expression";
return 0;
}
} else if (expr->IsNotEqual()) {
if (lhs_is_float_or_vec) {
op = spv::Op::OpFOrdNotEqual;
} else if (lhs_is_bool_or_vec) {
op = spv::Op::OpLogicalNotEqual;
} else if (lhs_is_integer_or_vec) {
op = spv::Op::OpINotEqual;
} else {
TINT_ICE() << "invalid not-equal expression";
return 0;
}
} else if (expr->IsOr()) {
if (lhs_is_integer_or_vec) {
op = spv::Op::OpBitwiseOr;
} else if (lhs_is_bool_or_vec) {
op = spv::Op::OpLogicalOr;
} else {
TINT_ICE() << "invalid and expression";
return 0;
}
} else if (expr->IsShiftLeft()) {
op = spv::Op::OpShiftLeftLogical;
} else if (expr->IsShiftRight() && lhs_type->is_signed_integer_scalar_or_vector()) {
// A shift right with a signed LHS is an arithmetic shift.
op = spv::Op::OpShiftRightArithmetic;
} else if (expr->IsShiftRight()) {
op = spv::Op::OpShiftRightLogical;
} else if (expr->IsSubtract()) {
op = lhs_is_float_or_vec ? spv::Op::OpFSub : spv::Op::OpISub;
} else if (expr->IsXor()) {
op = spv::Op::OpBitwiseXor;
} else {
TINT_ICE() << "unknown binary expression";
return 0;
}
if (!push_function_inst(op, {Operand(type_id), result, Operand(lhs_id), Operand(rhs_id)})) {
return 0;
}
return result_id;
}
bool Builder::GenerateBlockStatement(const ast::BlockStatement* stmt) {
PushScope();
TINT_DEFER(PopScope());
return GenerateBlockStatementWithoutScoping(stmt);
}
bool Builder::GenerateBlockStatementWithoutScoping(const ast::BlockStatement* stmt) {
for (auto* block_stmt : stmt->statements) {
if (!GenerateStatement(block_stmt)) {
return false;
}
}
return true;
}
uint32_t Builder::GenerateCallExpression(const ast::CallExpression* expr) {
auto* call = builder_.Sem().Get<sem::Call>(expr);
auto* target = call->Target();
return Switch(
target, //
[&](const sem::Function* func) { return GenerateFunctionCall(call, func); },
[&](const sem::BuiltinFn* builtin) { return GenerateBuiltinCall(call, builtin); },
[&](const sem::ValueConversion*) {
return GenerateValueConstructorOrConversion(call, nullptr);
},
[&](const sem::ValueConstructor*) {
return GenerateValueConstructorOrConversion(call, nullptr);
}, //
TINT_ICE_ON_NO_MATCH);
}
uint32_t Builder::GenerateFunctionCall(const sem::Call* call, const sem::Function* fn) {
auto* expr = call->Declaration();
auto* ident = fn->Declaration()->name;
auto type_id = GenerateTypeIfNeeded(call->Type());
if (type_id == 0) {
return 0;
}
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
OperandList ops = {Operand(type_id), result};
auto func_id = func_symbol_to_id_[ident->symbol];
if (func_id == 0) {
TINT_ICE() << "unable to find called function: " + ident->symbol.Name();
return 0;
}
ops.push_back(Operand(func_id));
for (auto* arg : expr->args) {
auto id = GenerateExpression(arg);
if (id == 0) {
return 0;
}
ops.push_back(Operand(id));
}
if (!push_function_inst(spv::Op::OpFunctionCall, std::move(ops))) {
return 0;
}
return result_id;
}
uint32_t Builder::GenerateBuiltinCall(const sem::Call* call, const sem::BuiltinFn* builtin) {
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
auto result_type_id = GenerateTypeIfNeeded(builtin->ReturnType());
if (result_type_id == 0) {
return 0;
}
if (builtin->IsFineDerivative() || builtin->IsCoarseDerivative()) {
module_.PushCapability(SpvCapabilityDerivativeControl);
}
if (builtin->IsImageQuery()) {
module_.PushCapability(SpvCapabilityImageQuery);
}
if (builtin->IsTexture()) {
if (!GenerateTextureBuiltin(call, builtin, Operand(result_type_id), result)) {
return 0;
}
return result_id;
}
if (builtin->IsBarrier()) {
if (!GenerateControlBarrierBuiltin(builtin)) {
return 0;
}
return result_id;
}
if (builtin->IsAtomic()) {
if (!GenerateAtomicBuiltin(call, builtin, Operand(result_type_id), result)) {
return 0;
}
return result_id;
}
// Generates the SPIR-V ID for the expression for the indexed call argument,
// and loads it if necessary. Returns 0 on error.
auto get_arg_as_value_id = [&](size_t i, bool generate_load = true) -> uint32_t {
auto* arg = call->Arguments()[i];
auto* param = builtin->Parameters()[i];
auto val_id = GenerateExpression(arg->Declaration());
if (val_id == 0) {
return 0;
}
if (generate_load && !param->Type()->Is<core::type::Pointer>()) {
val_id = GenerateLoadIfNeeded(arg->Type(), val_id);
}
return val_id;
};
OperandList params = {Operand(result_type_id), result};
spv::Op op = spv::Op::OpNop;
// Pushes the arguments for a GlslStd450 extended instruction, and sets op
// to OpExtInst.
auto glsl_std450 = [&](uint32_t inst_id) {
auto set_id = GetGLSLstd450Import();
params.push_back(Operand(set_id));
params.push_back(Operand(inst_id));
op = spv::Op::OpExtInst;
};
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kAny:
if (builtin->Parameters()[0]->Type()->Is<core::type::Bool>()) {
// any(v: bool) just resolves to v.
return get_arg_as_value_id(0);
}
op = spv::Op::OpAny;
break;
case wgsl::BuiltinFn::kAll:
if (builtin->Parameters()[0]->Type()->Is<core::type::Bool>()) {
// all(v: bool) just resolves to v.
return get_arg_as_value_id(0);
}
op = spv::Op::OpAll;
break;
case wgsl::BuiltinFn::kArrayLength: {
auto* address_of = call->Arguments()[0]->Declaration()->As<ast::UnaryOpExpression>();
if (!address_of || address_of->op != core::UnaryOp::kAddressOf) {
TINT_ICE() << "arrayLength() expected pointer to member access, got " +
std::string(address_of->TypeInfo().name);
return 0;
}
auto* array_expr = address_of->expr;
auto* accessor = array_expr->As<ast::MemberAccessorExpression>();
if (!accessor) {
TINT_ICE() << "arrayLength() expected pointer to member access, got pointer to " +
std::string(array_expr->TypeInfo().name);
return 0;
}
auto struct_id = GenerateExpression(accessor->object);
if (struct_id == 0) {
return 0;
}
params.push_back(Operand(struct_id));
auto* type = TypeOf(accessor->object)->UnwrapRef();
if (!type->Is<core::type::Struct>()) {
TINT_ICE() << "invalid type (" + type->FriendlyName() +
") for runtime array length";
return 0;
}
// Runtime array must be the last member in the structure
params.push_back(
Operand(uint32_t(type->As<core::type::Struct>()->Members().Length() - 1)));
if (!push_function_inst(spv::Op::OpArrayLength, params)) {
return 0;
}
return result_id;
}
case wgsl::BuiltinFn::kCountOneBits:
op = spv::Op::OpBitCount;
break;
case wgsl::BuiltinFn::kDot: {
op = spv::Op::OpDot;
auto* vec_ty = builtin->Parameters()[0]->Type()->As<core::type::Vector>();
if (vec_ty->type()->is_integer_scalar()) {
// TODO(crbug.com/tint/1267): OpDot requires floating-point types, but
// WGSL also supports integer types. SPV_KHR_integer_dot_product adds
// support for integer vectors. Use it if it is available.
auto el_ty = Operand(GenerateTypeIfNeeded(vec_ty->type()));
auto vec_a = Operand(get_arg_as_value_id(0));
auto vec_b = Operand(get_arg_as_value_id(1));
if (std::get<uint32_t>(vec_a) == 0 || std::get<uint32_t>(vec_b) == 0) {
return 0;
}
auto sum = Operand(0u);
for (uint32_t i = 0; i < vec_ty->Width(); i++) {
auto a = result_op();
auto b = result_op();
auto mul = result_op();
if (!push_function_inst(spv::Op::OpCompositeExtract,
{el_ty, a, vec_a, Operand(i)}) ||
!push_function_inst(spv::Op::OpCompositeExtract,
{el_ty, b, vec_b, Operand(i)}) ||
!push_function_inst(spv::Op::OpIMul, {el_ty, mul, a, b})) {
return 0;
}
if (i == 0) {
sum = mul;
} else {
auto prev_sum = sum;
auto is_last_el = i == (vec_ty->Width() - 1);
sum = is_last_el ? Operand(result_id) : result_op();
if (!push_function_inst(spv::Op::OpIAdd, {el_ty, sum, prev_sum, mul})) {
return 0;
}
}
}
return result_id;
}
break;
}
case wgsl::BuiltinFn::kDpdx:
op = spv::Op::OpDPdx;
break;
case wgsl::BuiltinFn::kDpdxCoarse:
op = spv::Op::OpDPdxCoarse;
break;
case wgsl::BuiltinFn::kDpdxFine:
op = spv::Op::OpDPdxFine;
break;
case wgsl::BuiltinFn::kDpdy:
op = spv::Op::OpDPdy;
break;
case wgsl::BuiltinFn::kDpdyCoarse:
op = spv::Op::OpDPdyCoarse;
break;
case wgsl::BuiltinFn::kDpdyFine:
op = spv::Op::OpDPdyFine;
break;
case wgsl::BuiltinFn::kExtractBits:
op = builtin->Parameters()[0]->Type()->is_unsigned_integer_scalar_or_vector()
? spv::Op::OpBitFieldUExtract
: spv::Op::OpBitFieldSExtract;
break;
case wgsl::BuiltinFn::kFwidth:
op = spv::Op::OpFwidth;
break;
case wgsl::BuiltinFn::kFwidthCoarse:
op = spv::Op::OpFwidthCoarse;
break;
case wgsl::BuiltinFn::kFwidthFine:
op = spv::Op::OpFwidthFine;
break;
case wgsl::BuiltinFn::kInsertBits:
op = spv::Op::OpBitFieldInsert;
break;
case wgsl::BuiltinFn::kMix: {
auto std450 = Operand(GetGLSLstd450Import());
auto a_id = get_arg_as_value_id(0);
auto b_id = get_arg_as_value_id(1);
auto f_id = get_arg_as_value_id(2);
if (!a_id || !b_id || !f_id) {
return 0;
}
// If the interpolant is scalar but the objects are vectors, we need to
// splat the interpolant into a vector of the same size.
auto* result_vector_type = builtin->ReturnType()->As<core::type::Vector>();
if (result_vector_type && builtin->Parameters()[2]->Type()->Is<core::type::Scalar>()) {
f_id = GenerateSplat(f_id, builtin->Parameters()[0]->Type());
if (f_id == 0) {
return 0;
}
}
if (!push_function_inst(spv::Op::OpExtInst, {Operand(result_type_id), result, std450,
U32Operand(GLSLstd450FMix), Operand(a_id),
Operand(b_id), Operand(f_id)})) {
return 0;
}
return result_id;
}
case wgsl::BuiltinFn::kQuantizeToF16:
op = spv::Op::OpQuantizeToF16;
break;
case wgsl::BuiltinFn::kReverseBits:
op = spv::Op::OpBitReverse;
break;
case wgsl::BuiltinFn::kSelect: {
// Note: Argument order is different in WGSL and SPIR-V
auto cond_id = get_arg_as_value_id(2);
auto true_id = get_arg_as_value_id(1);
auto false_id = get_arg_as_value_id(0);
if (!cond_id || !true_id || !false_id) {
return 0;
}
// If the condition is scalar but the objects are vectors, we need to
// splat the condition into a vector of the same size.
// TODO(jrprice): If we're targeting SPIR-V 1.4, we don't need to do this.
auto* result_vector_type = builtin->ReturnType()->As<core::type::Vector>();
if (result_vector_type && builtin->Parameters()[2]->Type()->Is<core::type::Scalar>()) {
auto* bool_vec_ty = builder_.create<core::type::Vector>(
builder_.create<core::type::Bool>(), result_vector_type->Width());
if (!GenerateTypeIfNeeded(bool_vec_ty)) {
return 0;
}
cond_id = GenerateSplat(cond_id, bool_vec_ty);
if (cond_id == 0) {
return 0;
}
}
if (!push_function_inst(spv::Op::OpSelect,
{Operand(result_type_id), result, Operand(cond_id),
Operand(true_id), Operand(false_id)})) {
return 0;
}
return result_id;
}
case wgsl::BuiltinFn::kTranspose:
op = spv::Op::OpTranspose;
break;
case wgsl::BuiltinFn::kAbs:
if (builtin->ReturnType()->is_unsigned_integer_scalar_or_vector()) {
// abs() only operates on *signed* integers.
// This is a no-op for unsigned integers.
return get_arg_as_value_id(0);
}
if (builtin->ReturnType()->is_float_scalar_or_vector()) {
glsl_std450(GLSLstd450FAbs);
} else {
glsl_std450(GLSLstd450SAbs);
}
break;
case wgsl::BuiltinFn::kDot4I8Packed: {
DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions();
auto first_param_id = get_arg_as_value_id(0);
auto second_param_id = get_arg_as_value_id(1);
if (!push_function_inst(spv::Op::OpSDotKHR,
{Operand(result_type_id), result, Operand(first_param_id),
Operand(second_param_id),
Operand(static_cast<uint32_t>(
spv::PackedVectorFormat::PackedVectorFormat4x8BitKHR))})) {
return 0;
}
return result_id;
}
case wgsl::BuiltinFn::kDot4U8Packed: {
DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions();
auto first_param_id = get_arg_as_value_id(0);
auto second_param_id = get_arg_as_value_id(1);
if (!push_function_inst(spv::Op::OpUDotKHR,
{Operand(result_type_id), result, Operand(first_param_id),
Operand(second_param_id),
Operand(static_cast<uint32_t>(
spv::PackedVectorFormat::PackedVectorFormat4x8BitKHR))})) {
return 0;
}
return result_id;
}
case wgsl::BuiltinFn::kSubgroupBallot: {
module_.PushCapability(SpvCapabilityGroupNonUniformBallot);
if (!push_function_inst(
spv::Op::OpGroupNonUniformBallot,
{Operand(result_type_id), result,
Operand(GenerateConstantIfNeeded(ScalarConstant::U32(SpvScopeSubgroup))),
Operand(GenerateConstantIfNeeded(ScalarConstant::Bool(true)))})) {
return 0;
}
return result_id;
}
case wgsl::BuiltinFn::kSubgroupBroadcast: {
module_.PushCapability(SpvCapabilityGroupNonUniformBallot);
auto first_param_id = get_arg_as_value_id(0);
auto second_param_id = get_arg_as_value_id(1);
if (!push_function_inst(
spv::Op::OpGroupNonUniformBroadcast,
{Operand(result_type_id), result,
Operand(GenerateConstantIfNeeded(ScalarConstant::U32(SpvScopeSubgroup))),
Operand(first_param_id), Operand(second_param_id)})) {
return 0;
}
return result_id;
}
default: {
auto inst_id = builtin_to_glsl_method(builtin);
if (inst_id == 0) {
TINT_ICE() << "unknown method " + std::string(builtin->str());
return 0;
}
glsl_std450(inst_id);
break;
}
}
if (op == spv::Op::OpNop) {
TINT_ICE() << "unable to determine operator for: " + std::string(builtin->str());
return 0;
}
for (size_t i = 0; i < call->Arguments().Length(); i++) {
if (auto val_id = get_arg_as_value_id(i)) {
params.emplace_back(Operand(val_id));
} else {
return 0;
}
}
if (!push_function_inst(op, params)) {
return 0;
}
return result_id;
}
bool Builder::GenerateTextureBuiltin(const sem::Call* call,
const sem::BuiltinFn* builtin,
Operand result_type,
Operand result_id) {
using Usage = core::ParameterUsage;
auto& signature = builtin->Signature();
auto& arguments = call->Arguments();
// Generates the given expression, returning the operand ID
auto gen = [&](const sem::ValueExpression* expr) { return Operand(GenerateExpression(expr)); };
// Returns the argument with the given usage
auto arg = [&](Usage usage) {
int idx = signature.IndexOf(usage);
return (idx >= 0) ? arguments[static_cast<size_t>(idx)] : nullptr;
};
// Generates the argument with the given usage, returning the operand ID
auto gen_arg = [&](Usage usage) {
auto* argument = arg(usage);
if (TINT_UNLIKELY(!argument)) {
TINT_ICE() << "missing argument " << static_cast<int>(usage);
}
return gen(argument);
};
auto* texture = arg(Usage::kTexture);
if (TINT_UNLIKELY(!texture)) {
TINT_ICE() << "missing texture argument";
}
auto* texture_type = texture->Type()->UnwrapRef()->As<core::type::Texture>();
auto op = spv::Op::OpNop;
// Custom function to call after the texture-builtin op has been generated.
std::function<bool()> post_emission = [] { return true; };
// Populate the spirv_params with common parameters
OperandList spirv_params;
spirv_params.reserve(8); // Enough to fit most parameter lists
// Extra image operands, appended to spirv_params.
struct ImageOperand {
SpvImageOperandsMask mask;
Operand operand;
};
std::vector<ImageOperand> image_operands;
image_operands.reserve(4); // Enough to fit most parameter lists
// Appends `result_type` and `result_id` to `spirv_params`
auto append_result_type_and_id_to_spirv_params = [&] {
spirv_params.emplace_back(std::move(result_type));
spirv_params.emplace_back(std::move(result_id));
};
// Appends a result type and id to `spirv_params`, possibly adding a
// post_emission step.
//
// If the texture is a depth texture, then this function wraps the result of
// the op with a OpCompositeExtract to evaluate to the first element of the
// returned vector. This is done as the WGSL texture reading functions for
// depths return a single float scalar instead of a vector.
//
// If the texture is not a depth texture, then this function simply delegates
// to calling append_result_type_and_id_to_spirv_params().
auto append_result_type_and_id_to_spirv_params_for_read = [&] {
if (texture_type
->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) {
auto* f32 = builder_.create<core::type::F32>();
auto* spirv_result_type = builder_.create<core::type::Vector>(f32, 4u);
auto spirv_result = result_op();
post_emission = [=] {
return push_function_inst(spv::Op::OpCompositeExtract,
{result_type, result_id, spirv_result, Operand(0u)});
};
auto spirv_result_type_id = GenerateTypeIfNeeded(spirv_result_type);
if (spirv_result_type_id == 0) {
return false;
}
spirv_params.emplace_back(Operand(spirv_result_type_id));
spirv_params.emplace_back(spirv_result);
return true;
}
append_result_type_and_id_to_spirv_params();
return true;
};
// Appends a result type and id to `spirv_params`, by first swizzling the
// result of the op with `swizzle`.
auto append_result_type_and_id_to_spirv_params_swizzled = [&](uint32_t spirv_result_width,
std::vector<uint32_t> swizzle) {
if (swizzle.empty()) {
append_result_type_and_id_to_spirv_params();
} else {
// Assign post_emission to swizzle the result of the call to
// OpImageQuerySize[Lod].
auto* element_type = ElementTypeOf(call->Type());
auto spirv_result = result_op();
auto* spirv_result_type =
builder_.create<core::type::Vector>(element_type, spirv_result_width);
if (swizzle.size() > 1) {
post_emission = [=] {
OperandList operands{
result_type,
result_id,
spirv_result,
spirv_result,
};
for (auto idx : swizzle) {
operands.emplace_back(Operand(idx));
}
return push_function_inst(spv::Op::OpVectorShuffle, operands);
};
} else {
post_emission = [=] {
return push_function_inst(
spv::Op::OpCompositeExtract,
{result_type, result_id, spirv_result, Operand(swizzle[0])});
};
}
auto spirv_result_type_id = GenerateTypeIfNeeded(spirv_result_type);
if (spirv_result_type_id == 0) {
return false;
}
spirv_params.emplace_back(Operand(spirv_result_type_id));
spirv_params.emplace_back(spirv_result);
}
return true;
};
auto append_coords_to_spirv_params = [&]() -> bool {
if (auto* array_index = arg(Usage::kArrayIndex)) {
// Array index needs to be appended to the coordinates.
auto* packed = tint::wgsl::AppendVector(&builder_, arg(Usage::kCoords)->Declaration(),
array_index->Declaration());
auto param = GenerateExpression(packed);
if (param == 0) {
return false;
}
spirv_params.emplace_back(Operand(param));
} else {
spirv_params.emplace_back(gen_arg(Usage::kCoords)); // coordinates
}
return true;
};
auto append_image_and_coords_to_spirv_params = [&]() -> bool {
auto sampler_param = gen_arg(Usage::kSampler);
auto texture_param = gen_arg(Usage::kTexture);
auto sampled_image = GenerateSampledImage(texture_type, texture_param, sampler_param);
// Populate the spirv_params with the common parameters
spirv_params.emplace_back(Operand(sampled_image)); // sampled image
return append_coords_to_spirv_params();
};
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kTextureDimensions: {
// Number of returned elements from OpImageQuerySize[Lod] may not match
// those of textureDimensions().
// This might be due to an extra vector scalar describing the number of
// array elements or textureDimensions() returning a vec3 for cubes
// when only width / height is returned by OpImageQuerySize[Lod]
// (see https://github.com/gpuweb/gpuweb/issues/1345).
// Handle these mismatches by swizzling the returned vector.
std::vector<uint32_t> swizzle;
uint32_t spirv_dims = 0;
switch (texture_type->dim()) {
case core::type::TextureDimension::kNone:
TINT_ICE() << "texture dimension is kNone";
return false;
case core::type::TextureDimension::k1d:
case core::type::TextureDimension::k2d:
case core::type::TextureDimension::k3d:
case core::type::TextureDimension::kCube:
break; // No swizzle needed
case core::type::TextureDimension::kCubeArray:
case core::type::TextureDimension::k2dArray:
swizzle = {0, 1}; // Strip array index
spirv_dims = 3; // [width, height, array_count]
break;
}
if (!append_result_type_and_id_to_spirv_params_swizzled(spirv_dims, swizzle)) {
return false;
}
spirv_params.emplace_back(gen_arg(Usage::kTexture));
if (texture_type->IsAnyOf<core::type::MultisampledTexture, //
core::type::DepthMultisampledTexture, //
core::type::StorageTexture>()) {
op = spv::Op::OpImageQuerySize;
} else if (auto* level = arg(Usage::kLevel)) {
op = spv::Op::OpImageQuerySizeLod;
spirv_params.emplace_back(gen(level));
} else {
op = spv::Op::OpImageQuerySizeLod;
spirv_params.emplace_back(
Operand(GenerateConstantIfNeeded(ScalarConstant::I32(0))));
}
break;
}
case wgsl::BuiltinFn::kTextureNumLayers: {
uint32_t spirv_dims = 0;
switch (texture_type->dim()) {
default:
TINT_ICE() << "texture is not arrayed";
return false;
case core::type::TextureDimension::k2dArray:
case core::type::TextureDimension::kCubeArray:
spirv_dims = 3;
break;
}
// OpImageQuerySize[Lod] packs the array count as the last element of the
// returned vector. Extract this.
if (!append_result_type_and_id_to_spirv_params_swizzled(spirv_dims, {spirv_dims - 1})) {
return false;
}
spirv_params.emplace_back(gen_arg(Usage::kTexture));
if (texture_type->Is<core::type::MultisampledTexture>() ||
texture_type->Is<core::type::StorageTexture>()) {
op = spv::Op::OpImageQuerySize;
} else {
op = spv::Op::OpImageQuerySizeLod;
spirv_params.emplace_back(
Operand(GenerateConstantIfNeeded(ScalarConstant::I32(0))));
}
break;
}
case wgsl::BuiltinFn::kTextureNumLevels: {
op = spv::Op::OpImageQueryLevels;
append_result_type_and_id_to_spirv_params();
spirv_params.emplace_back(gen_arg(Usage::kTexture));
break;
}
case wgsl::BuiltinFn::kTextureNumSamples: {
op = spv::Op::OpImageQuerySamples;
append_result_type_and_id_to_spirv_params();
spirv_params.emplace_back(gen_arg(Usage::kTexture));
break;
}
case wgsl::BuiltinFn::kTextureLoad: {
op = texture_type->Is<core::type::StorageTexture>() ? spv::Op::OpImageRead
: spv::Op::OpImageFetch;
append_result_type_and_id_to_spirv_params_for_read();
spirv_params.emplace_back(gen_arg(Usage::kTexture));
if (!append_coords_to_spirv_params()) {
return false;
}
if (auto* level = arg(Usage::kLevel)) {
image_operands.emplace_back(ImageOperand{SpvImageOperandsLodMask, gen(level)});
}
if (auto* sample_index = arg(Usage::kSampleIndex)) {
image_operands.emplace_back(
ImageOperand{SpvImageOperandsSampleMask, gen(sample_index)});
}
break;
}
case wgsl::BuiltinFn::kTextureStore: {
op = spv::Op::OpImageWrite;
spirv_params.emplace_back(gen_arg(Usage::kTexture));
if (!append_coords_to_spirv_params()) {
return false;
}
spirv_params.emplace_back(gen_arg(Usage::kValue));
break;
}
case wgsl::BuiltinFn::kTextureGather: {
op = spv::Op::OpImageGather;
append_result_type_and_id_to_spirv_params();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
if (signature.IndexOf(Usage::kComponent) < 0) {
spirv_params.emplace_back(
Operand(GenerateConstantIfNeeded(ScalarConstant::I32(0))));
} else {
spirv_params.emplace_back(gen_arg(Usage::kComponent));
}
break;
}
case wgsl::BuiltinFn::kTextureGatherCompare: {
op = spv::Op::OpImageDrefGather;
append_result_type_and_id_to_spirv_params();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
spirv_params.emplace_back(gen_arg(Usage::kDepthRef));
break;
}
case wgsl::BuiltinFn::kTextureSample: {
op = spv::Op::OpImageSampleImplicitLod;
append_result_type_and_id_to_spirv_params_for_read();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
break;
}
case wgsl::BuiltinFn::kTextureSampleBias: {
op = spv::Op::OpImageSampleImplicitLod;
append_result_type_and_id_to_spirv_params_for_read();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
image_operands.emplace_back(
ImageOperand{SpvImageOperandsBiasMask, gen_arg(Usage::kBias)});
break;
}
case wgsl::BuiltinFn::kTextureSampleLevel: {
op = spv::Op::OpImageSampleExplicitLod;
append_result_type_and_id_to_spirv_params_for_read();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
auto level = Operand(0u);
if (arg(Usage::kLevel)
->Type()
->UnwrapRef()
->IsAnyOf<core::type::I32, core::type::U32>()) {
// Depth textures have i32 or u32 parameters for the level, but SPIR-V expects f32.
// Cast.
auto f32_type_id = GenerateTypeIfNeeded(builder_.create<core::type::F32>());
if (f32_type_id == 0) {
return 0;
}
level = result_op();
if (!push_function_inst(spv::Op::OpConvertSToF,
{Operand(f32_type_id), level, gen_arg(Usage::kLevel)})) {
return 0;
}
} else {
level = gen_arg(Usage::kLevel);
}
image_operands.emplace_back(ImageOperand{SpvImageOperandsLodMask, level});
break;
}
case wgsl::BuiltinFn::kTextureSampleGrad: {
op = spv::Op::OpImageSampleExplicitLod;
append_result_type_and_id_to_spirv_params_for_read();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
image_operands.emplace_back(
ImageOperand{SpvImageOperandsGradMask, gen_arg(Usage::kDdx)});
image_operands.emplace_back(
ImageOperand{SpvImageOperandsGradMask, gen_arg(Usage::kDdy)});
break;
}
case wgsl::BuiltinFn::kTextureSampleCompare: {
op = spv::Op::OpImageSampleDrefImplicitLod;
append_result_type_and_id_to_spirv_params();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
spirv_params.emplace_back(gen_arg(Usage::kDepthRef));
break;
}
case wgsl::BuiltinFn::kTextureSampleCompareLevel: {
op = spv::Op::OpImageSampleDrefExplicitLod;
append_result_type_and_id_to_spirv_params();
if (!append_image_and_coords_to_spirv_params()) {
return false;
}
spirv_params.emplace_back(gen_arg(Usage::kDepthRef));
image_operands.emplace_back(
ImageOperand{SpvImageOperandsLodMask,
Operand(GenerateConstantIfNeeded(ScalarConstant::F32(0.0)))});
break;
}
default:
TINT_UNREACHABLE();
return false;
}
if (auto* offset = arg(Usage::kOffset)) {
image_operands.emplace_back(ImageOperand{SpvImageOperandsConstOffsetMask, gen(offset)});
}
if (!image_operands.empty()) {
// Use a stable sort to preserve the order of the Grad dpdx and dpdy
// operands.
std::stable_sort(image_operands.begin(), image_operands.end(),
[](auto& a, auto& b) { return a.mask < b.mask; });
uint32_t mask = 0;
for (auto& image_operand : image_operands) {
mask |= image_operand.mask;
}
spirv_params.emplace_back(Operand(mask));
for (auto& image_operand : image_operands) {
spirv_params.emplace_back(image_operand.operand);
}
}
if (op == spv::Op::OpNop) {
TINT_ICE() << "unable to determine operator for: " + std::string(builtin->str());
return false;
}
if (!push_function_inst(op, spirv_params)) {
return false;
}
return post_emission();
}
bool Builder::GenerateControlBarrierBuiltin(const sem::BuiltinFn* builtin) {
auto const op = spv::Op::OpControlBarrier;
uint32_t execution = 0;
uint32_t memory = 0;
uint32_t semantics = 0;
// TODO(crbug.com/tint/661): Combine sequential barriers to a single
// instruction.
if (builtin->Fn() == wgsl::BuiltinFn::kWorkgroupBarrier) {
execution = static_cast<uint32_t>(spv::Scope::Workgroup);
memory = static_cast<uint32_t>(spv::Scope::Workgroup);
semantics = static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory);
} else if (builtin->Fn() == wgsl::BuiltinFn::kStorageBarrier) {
execution = static_cast<uint32_t>(spv::Scope::Workgroup);
memory = static_cast<uint32_t>(spv::Scope::Workgroup);
semantics = static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<uint32_t>(spv::MemorySemanticsMask::UniformMemory);
} else if (builtin->Fn() == wgsl::BuiltinFn::kTextureBarrier) {
execution = static_cast<uint32_t>(spv::Scope::Workgroup);
memory = static_cast<uint32_t>(spv::Scope::Workgroup);
semantics = static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
static_cast<uint32_t>(spv::MemorySemanticsMask::ImageMemory);
} else {
TINT_ICE() << "unexpected barrier builtin type " << builtin->Fn();
return false;
}
auto execution_id = GenerateConstantIfNeeded(ScalarConstant::U32(execution));
auto memory_id = GenerateConstantIfNeeded(ScalarConstant::U32(memory));
auto semantics_id = GenerateConstantIfNeeded(ScalarConstant::U32(semantics));
if (execution_id == 0 || memory_id == 0 || semantics_id == 0) {
return false;
}
return push_function_inst(op, {
Operand(execution_id),
Operand(memory_id),
Operand(semantics_id),
});
}
bool Builder::GenerateAtomicBuiltin(const sem::Call* call,
const sem::BuiltinFn* builtin,
Operand result_type,
Operand result_id) {
auto is_value_signed = [&] { return builtin->Parameters()[1]->Type()->Is<core::type::I32>(); };
auto address_space =
builtin->Parameters()[0]->Type()->As<core::type::Pointer>()->AddressSpace();
uint32_t memory_id = 0;
switch (builtin->Parameters()[0]->Type()->As<core::type::Pointer>()->AddressSpace()) {
case core::AddressSpace::kWorkgroup:
memory_id = GenerateConstantIfNeeded(
ScalarConstant::U32(static_cast<uint32_t>(spv::Scope::Workgroup)));
break;
case core::AddressSpace::kStorage:
memory_id = GenerateConstantIfNeeded(
ScalarConstant::U32(static_cast<uint32_t>(spv::Scope::Device)));
break;
default:
TINT_UNREACHABLE() << "unhandled atomic address space " << address_space;
return false;
}
if (memory_id == 0) {
return false;
}
uint32_t semantics_id = GenerateConstantIfNeeded(
ScalarConstant::U32(static_cast<uint32_t>(spv::MemorySemanticsMask::MaskNone)));
if (semantics_id == 0) {
return false;
}
uint32_t pointer_id = GenerateExpression(call->Arguments()[0]);
if (pointer_id == 0) {
return false;
}
uint32_t value_id = 0;
if (call->Arguments().Length() > 1) {
value_id = GenerateExpression(call->Arguments().Back());
if (value_id == 0) {
return false;
}
}
Operand pointer = Operand(pointer_id);
Operand value = Operand(value_id);
Operand memory = Operand(memory_id);
Operand semantics = Operand(semantics_id);
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kAtomicLoad:
return push_function_inst(spv::Op::OpAtomicLoad, {
result_type,
result_id,
pointer,
memory,
semantics,
});
case wgsl::BuiltinFn::kAtomicStore:
return push_function_inst(spv::Op::OpAtomicStore, {
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicAdd:
return push_function_inst(spv::Op::OpAtomicIAdd, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicSub:
return push_function_inst(spv::Op::OpAtomicISub, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicMax:
return push_function_inst(
is_value_signed() ? spv::Op::OpAtomicSMax : spv::Op::OpAtomicUMax, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicMin:
return push_function_inst(
is_value_signed() ? spv::Op::OpAtomicSMin : spv::Op::OpAtomicUMin, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicAnd:
return push_function_inst(spv::Op::OpAtomicAnd, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicOr:
return push_function_inst(spv::Op::OpAtomicOr, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicXor:
return push_function_inst(spv::Op::OpAtomicXor, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicExchange:
return push_function_inst(spv::Op::OpAtomicExchange, {
result_type,
result_id,
pointer,
memory,
semantics,
value,
});
case wgsl::BuiltinFn::kAtomicCompareExchangeWeak: {
auto comparator = GenerateExpression(call->Arguments()[1]);
if (comparator == 0) {
return false;
}
auto* value_sem_type = call->Target()->Signature().parameters[2]->Type();
auto value_type = GenerateTypeIfNeeded(value_sem_type);
if (value_type == 0) {
return false;
}
auto* bool_sem_ty = builder_.create<core::type::Bool>();
auto bool_type = GenerateTypeIfNeeded(bool_sem_ty);
if (bool_type == 0) {
return false;
}
// original_value := OpAtomicCompareExchange(pointer, memory, semantics,
// semantics, value, comparator)
auto original_value = result_op();
if (!push_function_inst(spv::Op::OpAtomicCompareExchange, {
Operand(value_type),
original_value,
pointer,
memory,
semantics,
semantics,
value,
Operand(comparator),
})) {
return false;
}
// https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpAtomicCompareExchange
// According to SPIR-V spec, during the atomic steps of OpAtomicCompareExchange, the new
// value will be stored only if original value equals to comparator, and the result of
// OpAtomicCompareExchange is the original value. Therefore to check if the exchanging
// has been executed, we should compare the result original_value to comparator.
// values_equal := original_value == comparator
auto values_equal = result_op();
if (!push_function_inst(spv::Op::OpIEqual, {
Operand(bool_type),
values_equal,
original_value,
Operand(comparator),
})) {
return false;
}
// result := __atomic_compare_exchange_result<T>(original_value, values_equal)
return push_function_inst(spv::Op::OpCompositeConstruct, {
result_type,
result_id,
original_value,
values_equal,
});
}
default:
TINT_UNREACHABLE() << "unhandled atomic builtin " << builtin->Fn();
return false;
}
}
uint32_t Builder::GenerateSampledImage(const core::type::Type* texture_type,
Operand texture_operand,
Operand sampler_operand) {
// DepthTexture is always declared as SampledTexture.
// The Vulkan spec says: The "Depth" operand of OpTypeImage is ignored.
// In SPIRV, 0 means not depth, 1 means depth, and 2 means unknown.
// Using anything other than 0 is problematic on various Vulkan drivers.
if (auto* depthTextureType = texture_type->As<core::type::DepthTexture>()) {
texture_type = builder_.create<core::type::SampledTexture>(
depthTextureType->dim(), builder_.create<core::type::F32>());
}
uint32_t sampled_image_type_id =
tint::GetOrCreate(texture_type_to_sampled_image_type_id_, texture_type, [&] {
// We need to create the sampled image type and cache the result.
auto sampled_image_type = result_op();
auto texture_type_id = GenerateTypeIfNeeded(texture_type);
module_.PushType(spv::Op::OpTypeSampledImage,
{sampled_image_type, Operand(texture_type_id)});
return std::get<uint32_t>(sampled_image_type);
});
auto sampled_image = result_op();
if (!push_function_inst(spv::Op::OpSampledImage, {Operand(sampled_image_type_id), sampled_image,
texture_operand, sampler_operand})) {
return 0;
}
return std::get<uint32_t>(sampled_image);
}
uint32_t Builder::GenerateBitcastExpression(const ast::BitcastExpression* expr) {
auto result = result_op();
auto result_id = std::get<uint32_t>(result);
auto result_type_id = GenerateTypeIfNeeded(TypeOf(expr));
if (result_type_id == 0) {
return 0;
}
auto val_id = GenerateExpression(expr->expr);
if (val_id == 0) {
return 0;
}
// Bitcast does not allow same types, just emit a CopyObject
auto* to_type = TypeOf(expr)->UnwrapRef();
auto* from_type = TypeOf(expr->expr)->UnwrapRef();
if (to_type == from_type) {
if (!push_function_inst(spv::Op::OpCopyObject,
{Operand(result_type_id), result, Operand(val_id)})) {
return 0;
}
return result_id;
}
if (!push_function_inst(spv::Op::OpBitcast,
{Operand(result_type_id), result, Operand(val_id)})) {
return 0;
}
return result_id;
}
bool Builder::GenerateConditionalBlock(const ast::Expression* cond,
const ast::BlockStatement* true_body,
const ast::Statement* else_stmt) {
auto cond_id = GenerateExpression(cond);
if (cond_id == 0) {
return false;
}
auto merge_block = result_op();
auto merge_block_id = std::get<uint32_t>(merge_block);
if (!push_function_inst(spv::Op::OpSelectionMerge,
{Operand(merge_block_id), U32Operand(SpvSelectionControlMaskNone)})) {
return false;
}
auto true_block = result_op();
auto true_block_id = std::get<uint32_t>(true_block);
// if there are no more else statements we branch on false to the merge
// block otherwise we branch to the false block
auto false_block_id = else_stmt ? module_.NextId() : merge_block_id;
if (!push_function_inst(spv::Op::OpBranchConditional,
{Operand(cond_id), Operand(true_block_id), Operand(false_block_id)})) {
return false;
}
// Output true block
if (!GenerateLabel(true_block_id)) {
return false;
}
if (!GenerateBlockStatement(true_body)) {
return false;
}
// We only branch if the last element of the body didn't already branch.
if (InsideBasicBlock()) {
if (!push_function_inst(spv::Op::OpBranch, {Operand(merge_block_id)})) {
return false;
}
}
// Start the false block if needed
if (false_block_id != merge_block_id) {
if (!GenerateLabel(false_block_id)) {
return false;
}
// Handle the else case by just outputting the statements.
if (auto* block = else_stmt->As<ast::BlockStatement>()) {
if (!GenerateBlockStatement(block)) {
return false;
}
} else {
auto* elseif = else_stmt->As<ast::IfStatement>();
if (!GenerateConditionalBlock(elseif->condition, elseif->body,
elseif->else_statement)) {
return false;
}
}
if (InsideBasicBlock()) {
if (!push_function_inst(spv::Op::OpBranch, {Operand(merge_block_id)})) {
return false;
}
}
}
// Output the merge block
return GenerateLabel(merge_block_id);
}
bool Builder::GenerateIfStatement(const ast::IfStatement* stmt) {
if (!GenerateConditionalBlock(stmt->condition, stmt->body, stmt->else_statement)) {
return false;
}
return true;
}
bool Builder::GenerateSwitchStatement(const ast::SwitchStatement* stmt) {
auto merge_block = result_op();
auto merge_block_id = std::get<uint32_t>(merge_block);
merge_stack_.push_back(merge_block_id);
auto cond_id = GenerateExpression(stmt->condition);
if (cond_id == 0) {
return false;
}
auto default_block = result_op();
auto default_block_id = std::get<uint32_t>(default_block);
OperandList params = {Operand(cond_id), Operand(default_block_id)};
std::vector<uint32_t> case_ids;
for (const auto* item : stmt->body) {
auto block_id = default_block_id;
if (!item->ContainsDefault()) {
auto block = result_op();
block_id = std::get<uint32_t>(block);
}
case_ids.push_back(block_id);
// If this case statement is only a default selector skip adding the block
// as it will be done below.
if (item->selectors.Length() == 1 && item->ContainsDefault()) {
continue;
}
auto* sem = builder_.Sem().Get<sem::CaseStatement>(item);
for (auto* selector : sem->Selectors()) {
if (selector->IsDefault()) {
continue;
}
params.push_back(Operand(selector->Value()->ValueAs<u32>()));
params.push_back(Operand(block_id));
}
}
if (!push_function_inst(spv::Op::OpSelectionMerge,
{Operand(merge_block_id), U32Operand(SpvSelectionControlMaskNone)})) {
return false;
}
if (!push_function_inst(spv::Op::OpSwitch, params)) {
return false;
}
bool generated_default = false;
auto& body = stmt->body;
// We output the case statements in order they were entered in the original
// source. The branch is to the merge block which comes after the switch statement.
for (uint32_t i = 0; i < body.Length(); i++) {
auto* item = body[i];
if (item->ContainsDefault()) {
generated_default = true;
}
if (!GenerateLabel(case_ids[i])) {
return false;
}
if (!GenerateBlockStatement(item->body)) {
return false;
}
if (InsideBasicBlock()) {
if (!push_function_inst(spv::Op::OpBranch, {Operand(merge_block_id)})) {
return false;
}
}
}
if (!generated_default) {
if (!GenerateLabel(default_block_id)) {
return false;
}
if (!push_function_inst(spv::Op::OpBranch, {Operand(merge_block_id)})) {
return false;
}
}
merge_stack_.pop_back();
return GenerateLabel(merge_block_id);
}
bool Builder::GenerateReturnStatement(const ast::ReturnStatement* stmt) {
if (stmt->value) {
auto val_id = GenerateExpression(stmt->value);
if (val_id == 0) {
return false;
}
if (!push_function_inst(spv::Op::OpReturnValue, {Operand(val_id)})) {
return false;
}
} else {
if (!push_function_inst(spv::Op::OpReturn, {})) {
return false;
}
}
return true;
}
bool Builder::GenerateLoopStatement(const ast::LoopStatement* stmt) {
auto loop_header = result_op();
auto loop_header_id = std::get<uint32_t>(loop_header);
if (!push_function_inst(spv::Op::OpBranch, {Operand(loop_header_id)})) {
return false;
}
if (!GenerateLabel(loop_header_id)) {
return false;
}
auto merge_block = result_op();
auto merge_block_id = std::get<uint32_t>(merge_block);
auto continue_block = result_op();
auto continue_block_id = std::get<uint32_t>(continue_block);
auto body_block = result_op();
auto body_block_id = std::get<uint32_t>(body_block);
if (!push_function_inst(spv::Op::OpLoopMerge,
{Operand(merge_block_id), Operand(continue_block_id),
U32Operand(SpvLoopControlMaskNone)})) {
return false;
}
continue_stack_.push_back(continue_block_id);
merge_stack_.push_back(merge_block_id);
// Usually, the backedge is a simple branch. This will be modified if the
// backedge block in the continuing construct has an exiting edge.
backedge_stack_.emplace_back(spv::Op::OpBranch, OperandList{Operand(loop_header_id)});
if (!push_function_inst(spv::Op::OpBranch, {Operand(body_block_id)})) {
return false;
}
if (!GenerateLabel(body_block_id)) {
return false;
}
// We need variables from the body to be visible in the continuing block, so
// manage scope outside of GenerateBlockStatement.
{
PushScope();
TINT_DEFER(PopScope());
if (!GenerateBlockStatementWithoutScoping(stmt->body)) {
return false;
}
// We only branch if the last element of the body didn't already branch.
if (InsideBasicBlock()) {
if (!push_function_inst(spv::Op::OpBranch, {Operand(continue_block_id)})) {
return false;
}
}
if (!GenerateLabel(continue_block_id)) {
return false;
}
if (stmt->continuing && !stmt->continuing->Empty()) {
continuing_stack_.emplace_back(stmt->continuing->Last(), loop_header_id,
merge_block_id);
if (!GenerateBlockStatementWithoutScoping(stmt->continuing)) {
return false;
}
continuing_stack_.pop_back();
}
}
// Generate the backedge.
TINT_ASSERT(!backedge_stack_.empty());
const Backedge& backedge = backedge_stack_.back();
if (!push_function_inst(backedge.opcode, backedge.operands)) {
return false;
}
backedge_stack_.pop_back();
merge_stack_.pop_back();
continue_stack_.pop_back();
return GenerateLabel(merge_block_id);
}
bool Builder::GenerateStatement(const ast::Statement* stmt) {
return Switch(
stmt, [&](const ast::AssignmentStatement* a) { return GenerateAssignStatement(a); },
[&](const ast::BlockStatement* b) { return GenerateBlockStatement(b); },
[&](const ast::BreakStatement* b) { return GenerateBreakStatement(b); },
[&](const ast::BreakIfStatement* b) { return GenerateBreakIfStatement(b); },
[&](const ast::CallStatement* c) { return GenerateCallExpression(c->expr) != 0; },
[&](const ast::ContinueStatement* c) { return GenerateContinueStatement(c); },
[&](const ast::DiscardStatement* d) { return GenerateDiscardStatement(d); },
[&](const ast::IfStatement* i) { return GenerateIfStatement(i); },
[&](const ast::LoopStatement* l) { return GenerateLoopStatement(l); },
[&](const ast::ReturnStatement* r) { return GenerateReturnStatement(r); },
[&](const ast::SwitchStatement* s) { return GenerateSwitchStatement(s); },
[&](const ast::VariableDeclStatement* v) { return GenerateVariableDeclStatement(v); },
[&](const ast::ConstAssert*) {
return true; // Not emitted
}, //
TINT_ICE_ON_NO_MATCH);
}
bool Builder::GenerateVariableDeclStatement(const ast::VariableDeclStatement* stmt) {
return GenerateFunctionVariable(stmt->variable);
}
uint32_t Builder::GenerateTypeIfNeeded(const core::type::Type* type) {
if (type == nullptr) {
TINT_ICE() << "attempting to generate type from null type";
return 0;
}
// Atomics are a type in WGSL, but aren't a distinct type in SPIR-V.
// Just emit the type inside the atomic.
if (auto* atomic = type->As<core::type::Atomic>()) {
return GenerateTypeIfNeeded(atomic->Type());
}
// DepthTexture is always declared as SampledTexture.
// The Vulkan spec says: The "Depth" operand of OpTypeImage is ignored.
// In SPIRV, 0 means not depth, 1 means depth, and 2 means unknown.
// Using anything other than 0 is problematic on various Vulkan drivers.
if (auto* depthTextureType = type->As<core::type::DepthTexture>()) {
type = builder_.create<core::type::SampledTexture>(depthTextureType->dim(),
builder_.create<core::type::F32>());
} else if (auto* multisampledDepthTextureType =
type->As<core::type::DepthMultisampledTexture>()) {
type = builder_.create<core::type::MultisampledTexture>(multisampledDepthTextureType->dim(),
builder_.create<core::type::F32>());
}
// Pointers and references with differing accesses should not result in a
// different SPIR-V types, so we explicitly ignore the access.
// Pointers and References both map to a SPIR-V pointer type.
// Transform a Reference to a Pointer to prevent these having duplicated
// definitions in the generated SPIR-V. Note that nested pointers and
// references are not legal in WGSL, so only considering the top-level type is
// fine.
if (auto* ptr = type->As<core::type::Pointer>()) {
type = builder_.create<core::type::Pointer>(ptr->AddressSpace(), ptr->StoreType(),
core::Access::kReadWrite);
} else if (auto* ref = type->As<core::type::Reference>()) {
type = builder_.create<core::type::Pointer>(ref->AddressSpace(), ref->StoreType(),
core::Access::kReadWrite);
}
return tint::GetOrCreate(type_to_id_, type, [&]() -> uint32_t {
auto result = result_op();
auto id = std::get<uint32_t>(result);
bool ok = Switch(
type,
[&](const core::type::Array* arr) { //
return GenerateArrayType(arr, result);
},
[&](const core::type::Bool*) {
module_.PushType(spv::Op::OpTypeBool, {result});
return true;
},
[&](const core::type::F32*) {
module_.PushType(spv::Op::OpTypeFloat, {result, Operand(32u)});
return true;
},
[&](const core::type::F16*) {
module_.PushType(spv::Op::OpTypeFloat, {result, Operand(16u)});
return true;
},
[&](const core::type::I32*) {
module_.PushType(spv::Op::OpTypeInt, {result, Operand(32u), Operand(1u)});
return true;
},
[&](const core::type::Matrix* mat) { //
return GenerateMatrixType(mat, result);
},
[&](const core::type::Pointer* ptr) { //
return GeneratePointerType(ptr, result);
},
[&](const core::type::Reference* ref) { //
return GenerateReferenceType(ref, result);
},
[&](const core::type::Struct* str) { //
return GenerateStructType(str, result);
},
[&](const core::type::U32*) {
module_.PushType(spv::Op::OpTypeInt, {result, Operand(32u), Operand(0u)});
return true;
},
[&](const core::type::Vector* vec) { //
return GenerateVectorType(vec, result);
},
[&](const core::type::Void*) {
module_.PushType(spv::Op::OpTypeVoid, {result});
return true;
},
[&](const core::type::StorageTexture* tex) {
if (!GenerateTextureType(tex, result)) {
return false;
}
// Register all three access types of StorageTexture names. In
// SPIR-V, we must output a single type, while the variable is
// annotated with the access type. Doing this ensures we de-dupe.
type_to_id_[builder_.create<core::type::StorageTexture>(
tex->dim(), tex->texel_format(), core::Access::kRead, tex->type())] = id;
type_to_id_[builder_.create<core::type::StorageTexture>(
tex->dim(), tex->texel_format(), core::Access::kWrite, tex->type())] = id;
type_to_id_[builder_.create<core::type::StorageTexture>(
tex->dim(), tex->texel_format(), core::Access::kReadWrite, tex->type())] = id;
return true;
},
[&](const core::type::Texture* tex) { return GenerateTextureType(tex, result); },
[&](const core::type::Sampler* s) {
module_.PushType(spv::Op::OpTypeSampler, {result});
// Register both of the sampler type names. In SPIR-V they're the same
// sampler type, so we need to match that when we do the dedup check.
if (s->kind() == core::type::SamplerKind::kSampler) {
type_to_id_[builder_.create<core::type::Sampler>(
core::type::SamplerKind::kComparisonSampler)] = id;
} else {
type_to_id_[builder_.create<core::type::Sampler>(
core::type::SamplerKind::kSampler)] = id;
}
return true;
}, //
TINT_ICE_ON_NO_MATCH);
if (!ok) {
return 0;
}
return id;
});
}
bool Builder::GenerateTextureType(const core::type::Texture* texture, const Operand& result) {
if (TINT_UNLIKELY(texture->Is<core::type::ExternalTexture>())) {
TINT_ICE() << "Multiplanar external texture transform was not run.";
return false;
}
uint32_t array_literal = 0u;
const auto dim = texture->dim();
if (dim == core::type::TextureDimension::k2dArray ||
dim == core::type::TextureDimension::kCubeArray) {
array_literal = 1u;
}
uint32_t dim_literal = SpvDim2D;
if (dim == core::type::TextureDimension::k1d) {
dim_literal = SpvDim1D;
if (texture->Is<core::type::SampledTexture>()) {
module_.PushCapability(SpvCapabilitySampled1D);
} else if (texture->Is<core::type::StorageTexture>()) {
module_.PushCapability(SpvCapabilityImage1D);
}
}
if (dim == core::type::TextureDimension::k3d) {
dim_literal = SpvDim3D;
}
if (dim == core::type::TextureDimension::kCube ||
dim == core::type::TextureDimension::kCubeArray) {
dim_literal = SpvDimCube;
}
uint32_t ms_literal = 0u;
if (texture->IsAnyOf<core::type::MultisampledTexture, core::type::DepthMultisampledTexture>()) {
ms_literal = 1u;
}
uint32_t depth_literal = 0u;
// The Vulkan spec says: The "Depth" operand of OpTypeImage is ignored.
// In SPIRV, 0 means not depth, 1 means depth, and 2 means unknown.
// Using anything other than 0 is problematic on various Vulkan drivers.
uint32_t sampled_literal = 2u;
if (texture->IsAnyOf<core::type::MultisampledTexture, core::type::SampledTexture,
core::type::DepthTexture, core::type::DepthMultisampledTexture>()) {
sampled_literal = 1u;
}
if (dim == core::type::TextureDimension::kCubeArray) {
if (texture->IsAnyOf<core::type::SampledTexture, core::type::DepthTexture>()) {
module_.PushCapability(SpvCapabilitySampledCubeArray);
}
}
uint32_t type_id = Switch(
texture,
[&](const core::type::DepthTexture*) {
return GenerateTypeIfNeeded(builder_.create<core::type::F32>());
},
[&](const core::type::DepthMultisampledTexture*) {
return GenerateTypeIfNeeded(builder_.create<core::type::F32>());
},
[&](const core::type::SampledTexture* t) { return GenerateTypeIfNeeded(t->type()); },
[&](const core::type::MultisampledTexture* t) { return GenerateTypeIfNeeded(t->type()); },
[&](const core::type::StorageTexture* t) { return GenerateTypeIfNeeded(t->type()); }, //
TINT_ICE_ON_NO_MATCH);
if (type_id == 0u) {
return false;
}
uint32_t format_literal = SpvImageFormat_::SpvImageFormatUnknown;
if (auto* t = texture->As<core::type::StorageTexture>()) {
format_literal = convert_texel_format_to_spv(t->texel_format());
}
module_.PushType(spv::Op::OpTypeImage,
{result, Operand(type_id), Operand(dim_literal), Operand(depth_literal),
Operand(array_literal), Operand(ms_literal), Operand(sampled_literal),
Operand(format_literal)});
return true;
}
bool Builder::GenerateArrayType(const core::type::Array* arr, const Operand& result) {
auto elem_type = GenerateTypeIfNeeded(arr->ElemType());
if (elem_type == 0) {
return false;
}
auto result_id = std::get<uint32_t>(result);
if (arr->Count()->Is<core::type::RuntimeArrayCount>()) {
module_.PushType(spv::Op::OpTypeRuntimeArray, {result, Operand(elem_type)});
} else {
auto count = arr->ConstantCount();
if (!count) {
TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
return static_cast<uint32_t>(0);
}
auto len_id = GenerateConstantIfNeeded(ScalarConstant::U32(count.value()));
if (len_id == 0) {
return false;
}
module_.PushType(spv::Op::OpTypeArray, {result, Operand(elem_type), Operand(len_id)});
}
module_.PushAnnot(
spv::Op::OpDecorate,
{Operand(result_id), U32Operand(SpvDecorationArrayStride), Operand(arr->Stride())});
return true;
}
bool Builder::GenerateMatrixType(const core::type::Matrix* mat, const Operand& result) {
auto* col_type = builder_.create<core::type::Vector>(mat->type(), mat->rows());
auto col_type_id = GenerateTypeIfNeeded(col_type);
if (has_error()) {
return false;
}
module_.PushType(spv::Op::OpTypeMatrix,
{result, Operand(col_type_id), Operand(mat->columns())});
return true;
}
bool Builder::GeneratePointerType(const core::type::Pointer* ptr, const Operand& result) {
auto subtype_id = GenerateTypeIfNeeded(ptr->StoreType());
if (subtype_id == 0) {
return false;
}
auto stg_class = ConvertAddressSpace(ptr->AddressSpace());
if (stg_class == SpvStorageClassMax) {
TINT_ICE() << "invalid address space for pointer";
return false;
}
module_.PushType(spv::Op::OpTypePointer, {result, U32Operand(stg_class), Operand(subtype_id)});
return true;
}
bool Builder::GenerateReferenceType(const core::type::Reference* ref, const Operand& result) {
auto subtype_id = GenerateTypeIfNeeded(ref->StoreType());
if (subtype_id == 0) {
return false;
}
auto stg_class = ConvertAddressSpace(ref->AddressSpace());
if (stg_class == SpvStorageClassMax) {
TINT_ICE() << "invalid address space for reference";
return false;
}
module_.PushType(spv::Op::OpTypePointer, {result, U32Operand(stg_class), Operand(subtype_id)});
return true;
}
bool Builder::GenerateStructType(const core::type::Struct* struct_type, const Operand& result) {
auto struct_id = std::get<uint32_t>(result);
if (struct_type->Name().IsValid()) {
module_.PushDebug(spv::Op::OpName,
{Operand(struct_id), Operand(struct_type->Name().Name())});
}
OperandList ops;
ops.push_back(result);
if (auto* sem_str = struct_type->As<sem::Struct>()) {
auto* decl = sem_str->Declaration();
if (ast::HasAttribute<ast::transform::AddBlockAttribute::BlockAttribute>(
decl->attributes)) {
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(struct_id), U32Operand(SpvDecorationBlock)});
}
}
for (uint32_t i = 0; i < struct_type->Members().Length(); ++i) {
auto mem_id = GenerateStructMember(struct_id, i, struct_type->Members()[i]);
if (mem_id == 0) {
return false;
}
ops.push_back(Operand(mem_id));
}
module_.PushType(spv::Op::OpTypeStruct, std::move(ops));
return true;
}
uint32_t Builder::GenerateStructMember(uint32_t struct_id,
uint32_t idx,
const core::type::StructMember* member) {
module_.PushDebug(spv::Op::OpMemberName,
{Operand(struct_id), Operand(idx), Operand(member->Name().Name())});
// Note: This will generate layout annotations for *all* structs, whether or
// not they are used in host-shareable variables. This is officially ok in
// SPIR-V 1.0 through 1.3. If / when we migrate to using SPIR-V 1.4 we'll have
// to only generate the layout info for structs used for certain storage
// classes.
module_.PushAnnot(spv::Op::OpMemberDecorate,
{Operand(struct_id), Operand(idx), U32Operand(SpvDecorationOffset),
Operand(member->Offset())});
// Infer and emit matrix layout.
auto* matrix_type = GetNestedMatrixType(member->Type());
if (matrix_type) {
module_.PushAnnot(spv::Op::OpMemberDecorate,
{Operand(struct_id), Operand(idx), U32Operand(SpvDecorationColMajor)});
const uint32_t scalar_elem_size = matrix_type->type()->Size();
const uint32_t effective_row_count = (matrix_type->rows() == 2) ? 2 : 4;
module_.PushAnnot(spv::Op::OpMemberDecorate,
{Operand(struct_id), Operand(idx), U32Operand(SpvDecorationMatrixStride),
Operand(effective_row_count * scalar_elem_size)});
}
return GenerateTypeIfNeeded(member->Type());
}
bool Builder::GenerateVectorType(const core::type::Vector* vec, const Operand& result) {
auto type_id = GenerateTypeIfNeeded(vec->type());
if (has_error()) {
return false;
}
module_.PushType(spv::Op::OpTypeVector, {result, Operand(type_id), Operand(vec->Width())});
return true;
}
SpvStorageClass Builder::ConvertAddressSpace(core::AddressSpace address_space) const {
switch (address_space) {
case core::AddressSpace::kIn:
return SpvStorageClassInput;
case core::AddressSpace::kOut:
return SpvStorageClassOutput;
case core::AddressSpace::kUniform:
return SpvStorageClassUniform;
case core::AddressSpace::kWorkgroup:
return SpvStorageClassWorkgroup;
case core::AddressSpace::kPushConstant:
return SpvStorageClassPushConstant;
case core::AddressSpace::kHandle:
return SpvStorageClassUniformConstant;
case core::AddressSpace::kStorage:
return SpvStorageClassStorageBuffer;
case core::AddressSpace::kPrivate:
return SpvStorageClassPrivate;
case core::AddressSpace::kFunction:
return SpvStorageClassFunction;
case core::AddressSpace::kPixelLocal:
case core::AddressSpace::kUndefined:
break;
}
TINT_UNREACHABLE() << "unhandled address space '" << address_space << "'";
return SpvStorageClassMax;
}
SpvBuiltIn Builder::ConvertBuiltin(core::BuiltinValue builtin, core::AddressSpace storage) {
switch (builtin) {
case core::BuiltinValue::kPosition:
if (storage == core::AddressSpace::kIn) {
return SpvBuiltInFragCoord;
} else if (TINT_LIKELY(storage == core::AddressSpace::kOut)) {
return SpvBuiltInPosition;
} else {
TINT_ICE() << "invalid address space for builtin";
break;
}
case core::BuiltinValue::kVertexIndex:
return SpvBuiltInVertexIndex;
case core::BuiltinValue::kInstanceIndex:
return SpvBuiltInInstanceIndex;
case core::BuiltinValue::kFrontFacing:
return SpvBuiltInFrontFacing;
case core::BuiltinValue::kFragDepth:
return SpvBuiltInFragDepth;
case core::BuiltinValue::kLocalInvocationId:
return SpvBuiltInLocalInvocationId;
case core::BuiltinValue::kLocalInvocationIndex:
return SpvBuiltInLocalInvocationIndex;
case core::BuiltinValue::kGlobalInvocationId:
return SpvBuiltInGlobalInvocationId;
case core::BuiltinValue::kPointSize:
return SpvBuiltInPointSize;
case core::BuiltinValue::kWorkgroupId:
return SpvBuiltInWorkgroupId;
case core::BuiltinValue::kNumWorkgroups:
return SpvBuiltInNumWorkgroups;
case core::BuiltinValue::kSampleIndex:
module_.PushCapability(SpvCapabilitySampleRateShading);
return SpvBuiltInSampleId;
case core::BuiltinValue::kSampleMask:
return SpvBuiltInSampleMask;
case core::BuiltinValue::kSubgroupInvocationId:
module_.PushCapability(SpvCapabilityGroupNonUniform);
return SpvBuiltInSubgroupLocalInvocationId;
case core::BuiltinValue::kSubgroupSize:
module_.PushCapability(SpvCapabilityGroupNonUniform);
return SpvBuiltInSubgroupSize;
case core::BuiltinValue::kUndefined:
break;
}
return SpvBuiltInMax;
}
void Builder::AddInterpolationDecorations(uint32_t id,
core::InterpolationType type,
core::InterpolationSampling sampling) {
switch (type) {
case core::InterpolationType::kLinear:
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(id), U32Operand(SpvDecorationNoPerspective)});
break;
case core::InterpolationType::kFlat:
module_.PushAnnot(spv::Op::OpDecorate, {Operand(id), U32Operand(SpvDecorationFlat)});
break;
case core::InterpolationType::kPerspective:
case core::InterpolationType::kUndefined:
break;
}
switch (sampling) {
case core::InterpolationSampling::kCentroid:
module_.PushAnnot(spv::Op::OpDecorate,
{Operand(id), U32Operand(SpvDecorationCentroid)});
break;
case core::InterpolationSampling::kSample:
module_.PushCapability(SpvCapabilitySampleRateShading);
module_.PushAnnot(spv::Op::OpDecorate, {Operand(id), U32Operand(SpvDecorationSample)});
break;
case core::InterpolationSampling::kCenter:
case core::InterpolationSampling::kUndefined:
break;
}
}
SpvImageFormat Builder::convert_texel_format_to_spv(const core::TexelFormat format) {
switch (format) {
case core::TexelFormat::kBgra8Unorm:
TINT_ICE() << "bgra8unorm should have been polyfilled to rgba8unorm";
return SpvImageFormatUnknown;
case core::TexelFormat::kR32Uint:
return SpvImageFormatR32ui;
case core::TexelFormat::kR32Sint:
return SpvImageFormatR32i;
case core::TexelFormat::kR32Float:
return SpvImageFormatR32f;
case core::TexelFormat::kRgba8Unorm:
return SpvImageFormatRgba8;
case core::TexelFormat::kRgba8Snorm:
return SpvImageFormatRgba8Snorm;
case core::TexelFormat::kRgba8Uint:
return SpvImageFormatRgba8ui;
case core::TexelFormat::kRgba8Sint:
return SpvImageFormatRgba8i;
case core::TexelFormat::kRg32Uint:
module_.PushCapability(SpvCapabilityStorageImageExtendedFormats);
return SpvImageFormatRg32ui;
case core::TexelFormat::kRg32Sint:
module_.PushCapability(SpvCapabilityStorageImageExtendedFormats);
return SpvImageFormatRg32i;
case core::TexelFormat::kRg32Float:
module_.PushCapability(SpvCapabilityStorageImageExtendedFormats);
return SpvImageFormatRg32f;
case core::TexelFormat::kRgba16Uint:
return SpvImageFormatRgba16ui;
case core::TexelFormat::kRgba16Sint:
return SpvImageFormatRgba16i;
case core::TexelFormat::kRgba16Float:
return SpvImageFormatRgba16f;
case core::TexelFormat::kRgba32Uint:
return SpvImageFormatRgba32ui;
case core::TexelFormat::kRgba32Sint:
return SpvImageFormatRgba32i;
case core::TexelFormat::kRgba32Float:
return SpvImageFormatRgba32f;
case core::TexelFormat::kUndefined:
return SpvImageFormatUnknown;
}
return SpvImageFormatUnknown;
}
bool Builder::push_function_inst(spv::Op op, const OperandList& operands) {
if (!current_function_) {
StringStream ss;
ss << "Internal error: trying to add SPIR-V instruction " << int(op)
<< " outside a function";
TINT_ICE() << ss.str();
return false;
}
current_function_.push_inst(op, operands);
return true;
}
bool Builder::InsideBasicBlock() const {
if (!current_function_) {
return false;
}
const auto& instructions = current_function_.instructions();
if (instructions.empty()) {
// The Function object does not explicitly represent its entry block
// label. So return *true* because an empty list means the only
// thing in the function is that entry block label.
return true;
}
const auto& inst = instructions.back();
switch (inst.opcode()) {
case spv::Op::OpBranch:
case spv::Op::OpBranchConditional:
case spv::Op::OpSwitch:
case spv::Op::OpReturn:
case spv::Op::OpReturnValue:
case spv::Op::OpUnreachable:
case spv::Op::OpKill:
case spv::Op::OpTerminateInvocation:
return false;
default:
break;
}
return true;
}
void Builder::DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions() {
module_.PushExtension("SPV_KHR_integer_dot_product");
module_.PushCapability(SpvCapabilityDotProductKHR);
module_.PushCapability(SpvCapabilityDotProductInput4x8BitPackedKHR);
}
Builder::ContinuingInfo::ContinuingInfo(const ast::Statement* the_last_statement,
uint32_t loop_id,
uint32_t break_id)
: last_statement(the_last_statement), loop_header_id(loop_id), break_target_id(break_id) {
TINT_ASSERT(last_statement != nullptr);
TINT_ASSERT(loop_header_id != 0u);
TINT_ASSERT(break_target_id != 0u);
}
Builder::Backedge::Backedge(spv::Op the_opcode, OperandList the_operands)
: opcode(the_opcode), operands(the_operands) {}
Builder::Backedge::Backedge(const Builder::Backedge& other) = default;
Builder::Backedge& Builder::Backedge::operator=(const Builder::Backedge& other) = default;
Builder::Backedge::~Backedge() = default;
Builder::Scope::Scope() = default;
Builder::Scope::Scope(const Scope&) = default;
Builder::Scope::~Scope() = default;
} // namespace tint::spirv::writer