blob: b70890f684402cc6b0630f640a31e93e420ac876 [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/hlsl/writer/ast_printer/ast_printer.h"
#include <cmath>
#include <functional>
#include <iomanip>
#include <utility>
#include <vector>
#include "src/tint/api/common/binding_point.h"
#include "src/tint/lang/core/constant/splat.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/multisampled_texture.h"
#include "src/tint/lang/core/type/sampled_texture.h"
#include "src/tint/lang/core/type/storage_texture.h"
#include "src/tint/lang/core/type/texture_dimension.h"
#include "src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h"
#include "src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h"
#include "src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h"
#include "src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h"
#include "src/tint/lang/hlsl/writer/ast_raise/pixel_local.h"
#include "src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h"
#include "src/tint/lang/hlsl/writer/common/option_helpers.h"
#include "src/tint/lang/wgsl/ast/call_statement.h"
#include "src/tint/lang/wgsl/ast/internal_attribute.h"
#include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
#include "src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h"
#include "src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h"
#include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
#include "src/tint/lang/wgsl/ast/transform/builtin_polyfill.h"
#include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
#include "src/tint/lang/wgsl/ast/transform/demote_to_helper.h"
#include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
#include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
#include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
#include "src/tint/lang/wgsl/ast/transform/manager.h"
#include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
#include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
#include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
#include "src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.h"
#include "src/tint/lang/wgsl/ast/transform/remove_phonies.h"
#include "src/tint/lang/wgsl/ast/transform/robustness.h"
#include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
#include "src/tint/lang/wgsl/ast/transform/unshadow.h"
#include "src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h"
#include "src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h"
#include "src/tint/lang/wgsl/ast/variable_decl_statement.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/block_statement.h"
#include "src/tint/lang/wgsl/sem/call.h"
#include "src/tint/lang/wgsl/sem/function.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/ice/ice.h"
#include "src/tint/utils/macros/compiler.h"
#include "src/tint/utils/macros/defer.h"
#include "src/tint/utils/macros/scoped_assignment.h"
#include "src/tint/utils/rtti/switch.h"
#include "src/tint/utils/strconv/float_to_string.h"
#include "src/tint/utils/text/string.h"
#include "src/tint/utils/text/string_stream.h"
using namespace tint::core::number_suffixes; // NOLINT
using namespace tint::core::fluent_types; // NOLINT
namespace tint::hlsl::writer {
namespace {
const char kTempNamePrefix[] = "tint_tmp";
const char* ImageFormatToRWtextureType(core::TexelFormat image_format) {
switch (image_format) {
case core::TexelFormat::kR8Unorm:
case core::TexelFormat::kBgra8Unorm:
case core::TexelFormat::kRgba8Unorm:
case core::TexelFormat::kRgba8Snorm:
case core::TexelFormat::kRgba16Float:
case core::TexelFormat::kR32Float:
case core::TexelFormat::kRg32Float:
case core::TexelFormat::kRgba32Float:
return "float4";
case core::TexelFormat::kRgba8Uint:
case core::TexelFormat::kRgba16Uint:
case core::TexelFormat::kR32Uint:
case core::TexelFormat::kRg32Uint:
case core::TexelFormat::kRgba32Uint:
return "uint4";
case core::TexelFormat::kRgba8Sint:
case core::TexelFormat::kRgba16Sint:
case core::TexelFormat::kR32Sint:
case core::TexelFormat::kRg32Sint:
case core::TexelFormat::kRgba32Sint:
return "int4";
default:
return nullptr;
}
}
void PrintF32(StringStream& out, float value) {
if (std::isinf(value)) {
out << "0.0f " << (value >= 0 ? "/* inf */" : "/* -inf */");
} else if (std::isnan(value)) {
out << "0.0f /* nan */";
} else {
out << tint::strconv::FloatToString(value) << "f";
}
}
void PrintF16(StringStream& out, float value) {
if (std::isinf(value)) {
out << "0.0h " << (value >= 0 ? "/* inf */" : "/* -inf */");
} else if (std::isnan(value)) {
out << "0.0h /* nan */";
} else {
out << tint::strconv::FloatToString(value) << "h";
}
}
// Helper for writing " : register(RX, spaceY)", where R is the register, X is
// the binding point binding value, and Y is the binding point group value.
struct RegisterAndSpace {
RegisterAndSpace(char r, BindingPoint bp) : reg(r), binding_point(bp) {}
const char reg;
BindingPoint const binding_point;
};
StringStream& operator<<(StringStream& s, const RegisterAndSpace& rs) {
s << " : register(" << rs.reg << rs.binding_point.binding;
// Omit the space if it's 0, as it's the default.
// SM 5.0 doesn't support spaces, so we don't emit them if group is 0 for better compatibility.
if (rs.binding_point.group == 0) {
s << ")";
} else {
s << ", space" << rs.binding_point.group << ")";
}
return s;
}
} // namespace
SanitizedResult::SanitizedResult() = default;
SanitizedResult::~SanitizedResult() = default;
SanitizedResult::SanitizedResult(SanitizedResult&&) = default;
SanitizedResult Sanitize(const Program& in, const Options& options) {
ast::transform::Manager manager;
ast::transform::DataMap data;
manager.Add<ast::transform::FoldConstants>();
manager.Add<ast::transform::DisableUniformityAnalysis>();
// ExpandCompoundAssignment must come before BuiltinPolyfill
manager.Add<ast::transform::ExpandCompoundAssignment>();
manager.Add<ast::transform::Unshadow>(); // Must come before DirectVariableAccess
// LocalizeStructArrayAssignment must come after:
// * SimplifyPointers, because it assumes assignment to arrays in structs are
// done directly, not indirectly.
// TODO(crbug.com/tint/1340): See if we can get rid of the duplicate
// SimplifyPointers transform. Can't do it right now because
// LocalizeStructArrayAssignment introduces pointers.
manager.Add<ast::transform::SimplifyPointers>();
manager.Add<LocalizeStructArrayAssignment>();
manager.Add<ast::transform::PromoteSideEffectsToDecl>();
if (!options.disable_robustness) {
// Robustness must come after PromoteSideEffectsToDecl
// Robustness must come before BuiltinPolyfill and CanonicalizeEntryPointIO
manager.Add<ast::transform::Robustness>();
ast::transform::Robustness::Config config = {};
config.bindings_ignored = std::unordered_set<BindingPoint>(
options.bindings.ignored_by_robustness_transform.cbegin(),
options.bindings.ignored_by_robustness_transform.cend());
// Direct3D guarantees to return zero for any resource that is accessed out of bounds, and
// according to the description of the assembly store_uav_typed, out of bounds addressing
// means nothing gets written to memory.
config.texture_action = ast::transform::Robustness::Action::kIgnore;
data.Add<ast::transform::Robustness::Config>(config);
}
ExternalTextureOptions external_texture_options{};
RemapperData remapper_data{};
ArrayLengthFromUniformOptions array_length_from_uniform_options{};
PopulateBindingRelatedOptions(options, remapper_data, external_texture_options,
array_length_from_uniform_options);
manager.Add<ast::transform::BindingRemapper>();
// D3D11 and 12 registers like `t3` and `c3` have the same bindingOffset number in
// the remapping but should not be considered a collision because they have
// different types.
data.Add<ast::transform::BindingRemapper::Remappings>(
remapper_data, options.bindings.access_controls, /* allow_collisions */ true);
// Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
// MultiplanarExternalTexture must come after BindingRemapper
data.Add<ast::transform::MultiplanarExternalTexture::NewBindingPoints>(
external_texture_options.bindings_map, /* may_collide */ true);
manager.Add<ast::transform::MultiplanarExternalTexture>();
{ // Builtin polyfills
ast::transform::BuiltinPolyfill::Builtins polyfills;
polyfills.acosh = ast::transform::BuiltinPolyfill::Level::kFull;
polyfills.asinh = true;
polyfills.atanh = ast::transform::BuiltinPolyfill::Level::kFull;
polyfills.bitshift_modulo = true;
polyfills.clamp_int = true;
// TODO(crbug.com/tint/1449): Some of these can map to HLSL's `firstbitlow`
// and `firstbithigh`.
polyfills.conv_f32_to_iu32 = true;
polyfills.count_leading_zeros = true;
polyfills.count_trailing_zeros = true;
polyfills.extract_bits = ast::transform::BuiltinPolyfill::Level::kFull;
polyfills.first_leading_bit = true;
polyfills.first_trailing_bit = true;
polyfills.fwidth_fine = true;
polyfills.insert_bits = ast::transform::BuiltinPolyfill::Level::kFull;
polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
polyfills.precise_float_mod = true;
polyfills.reflect_vec2_f32 = options.polyfill_reflect_vec2_f32;
polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
polyfills.workgroup_uniform_load = true;
polyfills.dot_4x8_packed = options.polyfill_dot_4x8_packed;
polyfills.pack_unpack_4x8 = options.polyfill_pack_unpack_4x8;
// Currently Pack4xU8Clamp() must be polyfilled because on latest DXC pack_clamp_u8()
// receives an int32_t4 as its input.
// See https://github.com/microsoft/DirectXShaderCompiler/issues/5091 for more details.
polyfills.pack_4xu8_clamp = true;
data.Add<ast::transform::BuiltinPolyfill::Config>(polyfills);
manager.Add<ast::transform::BuiltinPolyfill>(); // Must come before DirectVariableAccess
}
manager.Add<ast::transform::DirectVariableAccess>();
if (!options.disable_workgroup_init) {
// ZeroInitWorkgroupMemory must come before CanonicalizeEntryPointIO as
// ZeroInitWorkgroupMemory may inject new builtin parameters.
manager.Add<ast::transform::ZeroInitWorkgroupMemory>();
}
{
PixelLocal::Config cfg;
for (auto it : options.pixel_local_options.attachments) {
cfg.pls_member_to_rov_reg.Add(it.first, it.second);
}
for (auto it : options.pixel_local_options.attachment_formats) {
core::TexelFormat format = core::TexelFormat::kUndefined;
switch (it.second) {
case PixelLocalOptions::TexelFormat::kR32Sint:
format = core::TexelFormat::kR32Sint;
break;
case PixelLocalOptions::TexelFormat::kR32Uint:
format = core::TexelFormat::kR32Uint;
break;
case PixelLocalOptions::TexelFormat::kR32Float:
format = core::TexelFormat::kR32Float;
break;
default:
TINT_ICE() << "missing texel format for pixel local storage attachment";
}
cfg.pls_member_to_rov_format.Add(it.first, format);
}
cfg.rov_group_index = options.pixel_local_options.pixel_local_group_index;
data.Add<PixelLocal::Config>(cfg);
manager.Add<PixelLocal>();
}
// CanonicalizeEntryPointIO must come after Robustness
manager.Add<ast::transform::CanonicalizeEntryPointIO>();
if (options.truncate_interstage_variables) {
// When interstage_locations is empty, it means there's no user-defined interstage variables
// being used in the next stage. Still, HLSL compiler register mismatch could happen, if
// there's builtin inputs used in the next stage. So we still run
// TruncateInterstageVariables transform.
// TruncateInterstageVariables itself will skip when interstage_locations matches exactly
// with the current stage output.
// Build the config for internal TruncateInterstageVariables transform.
TruncateInterstageVariables::Config truncate_interstage_variables_cfg;
truncate_interstage_variables_cfg.interstage_locations =
std::move(options.interstage_locations);
manager.Add<TruncateInterstageVariables>();
data.Add<TruncateInterstageVariables::Config>(std::move(truncate_interstage_variables_cfg));
}
// NumWorkgroupsFromUniform must come after CanonicalizeEntryPointIO, as it
// assumes that num_workgroups builtins only appear as struct members and are
// only accessed directly via member accessors.
manager.Add<NumWorkgroupsFromUniform>();
manager.Add<ast::transform::VectorizeScalarMatrixInitializers>();
manager.Add<ast::transform::SimplifyPointers>();
manager.Add<ast::transform::RemovePhonies>();
// DemoteToHelper must come after CanonicalizeEntryPointIO, PromoteSideEffectsToDecl, and
// ExpandCompoundAssignment.
// TODO(crbug.com/tint/1752): This is only necessary when FXC is being used.
manager.Add<ast::transform::DemoteToHelper>();
// ArrayLengthFromUniform must come after SimplifyPointers as it assumes that the form of the
// array length argument is &var.array.
manager.Add<ast::transform::ArrayLengthFromUniform>();
// Build the config for the internal ArrayLengthFromUniform transform.
ast::transform::ArrayLengthFromUniform::Config array_length_from_uniform_cfg(
BindingPoint{array_length_from_uniform_options.ubo_binding.group,
array_length_from_uniform_options.ubo_binding.binding});
array_length_from_uniform_cfg.bindpoint_to_size_index =
std::move(array_length_from_uniform_options.bindpoint_to_size_index);
data.Add<ast::transform::ArrayLengthFromUniform::Config>(
std::move(array_length_from_uniform_cfg));
// DecomposeMemoryAccess must come after:
// * SimplifyPointers, as we cannot take the address of calls to
// DecomposeMemoryAccess::Intrinsic and we need to fold away the address-of and dereferences
// of `*(&(intrinsic_load()))` expressions.
// * RemovePhonies, as phonies can be assigned a pointer to a
// non-constructible buffer, or dynamic array, which DMA cannot cope with.
manager.Add<DecomposeMemoryAccess>();
// CalculateArrayLength must come after DecomposeMemoryAccess, as
// DecomposeMemoryAccess special-cases the arrayLength() intrinsic, which
// will be transformed by CalculateArrayLength
manager.Add<CalculateArrayLength>();
manager.Add<ast::transform::PromoteInitializersToLet>();
manager.Add<ast::transform::RemoveContinueInSwitch>();
manager.Add<ast::transform::AddEmptyEntryPoint>();
data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
data.Add<NumWorkgroupsFromUniform::Config>(options.root_constant_binding_point);
SanitizedResult result;
ast::transform::DataMap outputs;
result.program = manager.Run(in, data, outputs);
if (auto* res = outputs.Get<ast::transform::ArrayLengthFromUniform::Result>()) {
result.used_array_length_from_uniform_indices = std::move(res->used_size_indices);
}
return result;
}
ASTPrinter::ASTPrinter(const Program& program) : builder_(ProgramBuilder::Wrap(program)) {}
ASTPrinter::~ASTPrinter() = default;
bool ASTPrinter::Generate() {
if (!tint::wgsl::CheckSupportedExtensions(
"HLSL", builder_.AST(), diagnostics_,
Vector{
wgsl::Extension::kChromiumDisableUniformityAnalysis,
wgsl::Extension::kChromiumExperimentalPixelLocal,
wgsl::Extension::kChromiumExperimentalPushConstant,
wgsl::Extension::kChromiumExperimentalSubgroups,
wgsl::Extension::kChromiumInternalDualSourceBlending,
wgsl::Extension::kChromiumInternalGraphite,
wgsl::Extension::kF16,
wgsl::Extension::kDualSourceBlending,
})) {
return false;
}
const tint::TypeInfo* last_kind = nullptr;
size_t last_padding_line = 0;
auto* mod = builder_.Sem().Module();
for (auto* decl : mod->DependencyOrderedDeclarations()) {
if (decl->IsAnyOf<ast::Alias, ast::DiagnosticDirective, ast::Enable, ast::Requires,
ast::ConstAssert>()) {
continue; // These are not emitted.
}
// Emit a new line between declarations if the type of declaration has
// changed, or we're about to emit a function
auto* kind = &decl->TypeInfo();
if (current_buffer_->lines.size() != last_padding_line) {
if (last_kind && (last_kind != kind || decl->Is<ast::Function>())) {
Line();
last_padding_line = current_buffer_->lines.size();
}
}
last_kind = kind;
global_insertion_point_ = current_buffer_->lines.size();
bool ok = Switch(
decl,
[&](const ast::Variable* global) { //
return EmitGlobalVariable(global);
},
[&](const ast::Struct* str) {
auto* ty = builder_.Sem().Get(str);
auto address_space_uses = ty->AddressSpaceUsage();
if (address_space_uses.Count() !=
((address_space_uses.Contains(core::AddressSpace::kStorage) ? 1u : 0u) +
(address_space_uses.Contains(core::AddressSpace::kUniform) ? 1u : 0u))) {
// The structure is used as something other than a storage buffer or
// uniform buffer, so it needs to be emitted.
// Storage buffer are read and written to via a ByteAddressBuffer
// instead of true structure.
// Structures used as uniform buffer are read from an array of
// vectors instead of true structure.
return EmitStructType(current_buffer_, ty);
}
return true;
},
[&](const ast::Function* func) {
if (func->IsEntryPoint()) {
return EmitEntryPointFunction(func);
}
return EmitFunction(func);
}, //
TINT_ICE_ON_NO_MATCH);
if (!ok) {
return false;
}
}
if (!helpers_.lines.empty()) {
current_buffer_->Insert(helpers_, 0, 0);
}
return true;
}
bool ASTPrinter::EmitDynamicVectorAssignment(const ast::AssignmentStatement* stmt,
const core::type::Vector* vec) {
auto name = tint::GetOrAdd(dynamic_vector_write_, vec, [&]() -> std::string {
std::string fn = UniqueIdentifier("set_vector_element");
{
auto out = Line(&helpers_);
out << "void " << fn << "(inout ";
if (!EmitTypeAndName(out, vec, core::AddressSpace::kUndefined, core::Access::kUndefined,
"vec")) {
return "";
}
out << ", int idx, ";
if (!EmitTypeAndName(out, vec->type(), core::AddressSpace::kUndefined,
core::Access::kUndefined, "val")) {
return "";
}
out << ") {";
}
{
ScopedIndent si(&helpers_);
auto out = Line(&helpers_);
switch (vec->Width()) {
case 2:
out << "vec = (idx.xx == int2(0, 1)) ? val.xx : vec;";
break;
case 3:
out << "vec = (idx.xxx == int3(0, 1, 2)) ? val.xxx : vec;";
break;
case 4:
out << "vec = (idx.xxxx == int4(0, 1, 2, 3)) ? val.xxxx : vec;";
break;
default:
TINT_UNREACHABLE() << "invalid vector size " << vec->Width();
}
}
Line(&helpers_) << "}";
Line(&helpers_);
return fn;
});
if (name.empty()) {
return false;
}
auto* ast_access_expr = stmt->lhs->As<ast::IndexAccessorExpression>();
auto out = Line();
out << name << "(";
if (!EmitExpression(out, ast_access_expr->object)) {
return false;
}
out << ", ";
if (!EmitExpression(out, ast_access_expr->index)) {
return false;
}
out << ", ";
if (!EmitExpression(out, stmt->rhs)) {
return false;
}
out << ");";
return true;
}
bool ASTPrinter::EmitDynamicMatrixVectorAssignment(const ast::AssignmentStatement* stmt,
const core::type::Matrix* mat) {
auto name = tint::GetOrAdd(dynamic_matrix_vector_write_, mat, [&]() -> std::string {
std::string fn = UniqueIdentifier("set_matrix_column");
{
auto out = Line(&helpers_);
out << "void " << fn << "(inout ";
if (!EmitTypeAndName(out, mat, core::AddressSpace::kUndefined, core::Access::kUndefined,
"mat")) {
return "";
}
out << ", int col, ";
if (!EmitTypeAndName(out, mat->ColumnType(), core::AddressSpace::kUndefined,
core::Access::kUndefined, "val")) {
return "";
}
out << ") {";
}
{
ScopedIndent si(&helpers_);
Line(&helpers_) << "switch (col) {";
{
ScopedIndent si2(&helpers_);
for (uint32_t i = 0; i < mat->columns(); ++i) {
Line(&helpers_) << "case " << i << ": mat[" << i << "] = val; break;";
}
}
Line(&helpers_) << "}";
}
Line(&helpers_) << "}";
Line(&helpers_);
return fn;
});
if (name.empty()) {
return false;
}
auto* ast_access_expr = stmt->lhs->As<ast::IndexAccessorExpression>();
auto out = Line();
out << name << "(";
if (!EmitExpression(out, ast_access_expr->object)) {
return false;
}
out << ", ";
if (!EmitExpression(out, ast_access_expr->index)) {
return false;
}
out << ", ";
if (!EmitExpression(out, stmt->rhs)) {
return false;
}
out << ");";
return true;
}
bool ASTPrinter::EmitDynamicMatrixScalarAssignment(const ast::AssignmentStatement* stmt,
const core::type::Matrix* mat) {
auto* lhs_row_access = stmt->lhs->As<ast::IndexAccessorExpression>();
auto* lhs_col_access = lhs_row_access->object->As<ast::IndexAccessorExpression>();
auto name = tint::GetOrAdd(dynamic_matrix_scalar_write_, mat, [&]() -> std::string {
std::string fn = UniqueIdentifier("set_matrix_scalar");
{
auto out = Line(&helpers_);
out << "void " << fn << "(inout ";
if (!EmitTypeAndName(out, mat, core::AddressSpace::kUndefined, core::Access::kUndefined,
"mat")) {
return "";
}
out << ", int col, int row, ";
if (!EmitTypeAndName(out, mat->type(), core::AddressSpace::kUndefined,
core::Access::kUndefined, "val")) {
return "";
}
out << ") {";
}
{
ScopedIndent si(&helpers_);
Line(&helpers_) << "switch (col) {";
{
ScopedIndent si2(&helpers_);
for (uint32_t i = 0; i < mat->columns(); ++i) {
Line(&helpers_) << "case " << i << ":";
{
auto vec_name = "mat[" + std::to_string(i) + "]";
ScopedIndent si3(&helpers_);
{
auto out = Line(&helpers_);
switch (mat->rows()) {
case 2:
out << vec_name
<< " = (row.xx == int2(0, 1)) ? val.xx : " << vec_name
<< ";";
break;
case 3:
out << vec_name
<< " = (row.xxx == int3(0, 1, 2)) ? val.xxx : " << vec_name
<< ";";
break;
case 4:
out << vec_name
<< " = (row.xxxx == int4(0, 1, 2, 3)) ? val.xxxx : "
<< vec_name << ";";
break;
default: {
auto* vec = TypeOf(lhs_row_access->object)
->UnwrapPtrOrRef()
->As<core::type::Vector>();
TINT_UNREACHABLE() << "invalid vector size " << vec->Width();
}
}
}
Line(&helpers_) << "break;";
}
}
}
Line(&helpers_) << "}";
}
Line(&helpers_) << "}";
Line(&helpers_);
return fn;
});
if (name.empty()) {
return false;
}
auto out = Line();
out << name << "(";
if (!EmitExpression(out, lhs_col_access->object)) {
return false;
}
out << ", ";
if (!EmitExpression(out, lhs_col_access->index)) {
return false;
}
out << ", ";
if (!EmitExpression(out, lhs_row_access->index)) {
return false;
}
out << ", ";
if (!EmitExpression(out, stmt->rhs)) {
return false;
}
out << ");";
return true;
}
bool ASTPrinter::EmitIndexAccessor(StringStream& out, const ast::IndexAccessorExpression* expr) {
if (!EmitExpression(out, expr->object)) {
return false;
}
out << "[";
if (!EmitExpression(out, expr->index)) {
return false;
}
out << "]";
return true;
}
bool ASTPrinter::EmitBitcastCall(StringStream& out, const ast::CallExpression* call) {
auto* arg = call->args[0];
auto* src_type = TypeOf(arg)->UnwrapRef();
auto* dst_type = TypeOf(call);
auto* src_el_type = src_type->DeepestElement();
auto* dst_el_type = dst_type->DeepestElement();
if (!dst_el_type->is_integer_scalar() && !dst_el_type->is_float_scalar()) {
diagnostics_.AddError(Source{})
<< "Unable to do bitcast to type " << dst_el_type->FriendlyName();
return false;
}
// Handle identity bitcast.
if (src_type == dst_type) {
return EmitExpression(out, arg);
}
// Handle the f16 types using polyfill functions
if (src_el_type->Is<core::type::F16>() || dst_el_type->Is<core::type::F16>()) {
auto f16_bitcast_polyfill = [&]() {
if (src_el_type->Is<core::type::F16>()) {
// Source type must be vec2<f16> or vec4<f16>, since type f16 and vec3<f16> can only
// have identity bitcast.
auto* src_vec = src_type->As<core::type::Vector>();
TINT_ASSERT(src_vec);
TINT_ASSERT(((src_vec->Width() == 2u) || (src_vec->Width() == 4u)));
// Bitcast f16 types to others by converting the given f16 value to f32 and call
// f32tof16 to get the bits. This should be safe, because the convertion is precise
// for finite and infinite f16 value as they are exactly representable by f32, and
// WGSL spec allow any result if f16 value is NaN.
return tint::GetOrAdd(
bitcast_funcs_, BinaryType{{src_type, dst_type}}, [&]() -> std::string {
TextBuffer b;
TINT_DEFER(helpers_.Append(b));
auto fn_name = UniqueIdentifier(std::string("tint_bitcast_from_f16"));
{
auto decl = Line(&b);
if (!EmitTypeAndName(decl, dst_type, core::AddressSpace::kUndefined,
core::Access::kUndefined, fn_name)) {
return "";
}
{
ScopedParen sp(decl);
if (!EmitTypeAndName(decl, src_type, core::AddressSpace::kUndefined,
core::Access::kUndefined, "src")) {
return "";
}
}
decl << " {";
}
{
ScopedIndent si(&b);
{
Line(&b) << "uint" << src_vec->Width() << " r = f32tof16(float"
<< src_vec->Width() << "(src));";
{
auto s = Line(&b);
s << "return as";
if (!EmitType(s, dst_el_type, core::AddressSpace::kUndefined,
core::Access::kReadWrite, "")) {
return "";
}
s << "(";
switch (src_vec->Width()) {
case 2: {
s << "uint((r.x & 0xffff) | ((r.y & 0xffff) << 16))";
break;
}
case 4: {
s << "uint2((r.x & 0xffff) | ((r.y & 0xffff) << 16), "
"(r.z & 0xffff) | ((r.w & 0xffff) << 16))";
break;
}
}
s << ");";
}
}
}
Line(&b) << "}";
Line(&b);
return fn_name;
});
} else {
// Destination type must be vec2<f16> or vec4<f16>.
auto* dst_vec = dst_type->As<core::type::Vector>();
TINT_ASSERT((dst_vec && ((dst_vec->Width() == 2u) || (dst_vec->Width() == 4u)) &&
dst_el_type->Is<core::type::F16>()));
// Source type must be f32/i32/u32 or vec2<f32/i32/u32>.
auto* src_vec = src_type->As<core::type::Vector>();
TINT_ASSERT(
(src_type->IsAnyOf<core::type::I32, core::type::U32, core::type::F32>() ||
(src_vec && src_vec->Width() == 2u &&
src_el_type->IsAnyOf<core::type::I32, core::type::U32, core::type::F32>())));
std::string src_type_suffix = (src_vec ? "2" : "");
// Bitcast other types to f16 types by reinterpreting their bits as f16 using
// f16tof32, and convert the result f32 to f16. This should be safe, because the
// convertion is precise for finite and infinite f16 result value as they are
// exactly representable by f32, and WGSL spec allow any result if f16 result value
// would be NaN.
return tint::GetOrAdd(
bitcast_funcs_, BinaryType{{src_type, dst_type}}, [&]() -> std::string {
TextBuffer b;
TINT_DEFER(helpers_.Append(b));
auto fn_name = UniqueIdentifier(std::string("tint_bitcast_to_f16"));
{
auto decl = Line(&b);
if (!EmitTypeAndName(decl, dst_type, core::AddressSpace::kUndefined,
core::Access::kUndefined, fn_name)) {
return "";
}
{
ScopedParen sp(decl);
if (!EmitTypeAndName(decl, src_type, core::AddressSpace::kUndefined,
core::Access::kUndefined, "src")) {
return "";
}
}
decl << " {";
}
{
ScopedIndent si(&b);
{
// Convert the source to uint for f16tof32.
Line(&b) << "uint" << src_type_suffix << " v = asuint(src);";
// Reinterprete the low 16 bits and high 16 bits
Line(&b) << "float" << src_type_suffix
<< " t_low = f16tof32(v & 0xffff);";
Line(&b) << "float" << src_type_suffix
<< " t_high = f16tof32((v >> 16) & 0xffff);";
// Construct the result f16 vector
{
auto s = Line(&b);
s << "return ";
if (!EmitType(s, dst_type, core::AddressSpace::kUndefined,
core::Access::kReadWrite, "")) {
return "";
}
s << "(";
switch (dst_vec->Width()) {
case 2: {
s << "t_low.x, t_high.x";
break;
}
case 4: {
s << "t_low.x, t_high.x, t_low.y, t_high.y";
break;
}
}
s << ");";
}
}
}
Line(&b) << "}";
Line(&b);
return fn_name;
});
}
};
// Get or create the polyfill
auto fn = f16_bitcast_polyfill();
if (fn.empty()) {
return false;
}
// Call the polyfill
out << fn;
{
ScopedParen sp(out);
if (!EmitExpression(out, arg)) {
return false;
}
}
return true;
}
// Otherwise, bitcasting between non-f16 types.
TINT_ASSERT((!src_el_type->Is<core::type::F16>() && !dst_el_type->Is<core::type::F16>()));
out << "as";
if (!EmitType(out, dst_el_type, core::AddressSpace::kUndefined, core::Access::kReadWrite, "")) {
return false;
}
out << "(";
if (!EmitExpression(out, arg)) {
return false;
}
out << ")";
return true;
}
bool ASTPrinter::EmitAssign(const ast::AssignmentStatement* stmt) {
if (auto* lhs_access = stmt->lhs->As<ast::IndexAccessorExpression>()) {
auto validate_obj_not_pointer = [&](const core::type::Type* object_ty) {
if (TINT_UNLIKELY(object_ty->Is<core::type::Pointer>())) {
TINT_ICE() << "lhs of index accessor should not be a pointer. These should have "
"been removed by transforms such as SimplifyPointers, "
"DecomposeMemoryAccess, and DirectVariableAccess";
}
return true;
};
// BUG(crbug.com/tint/1333): work around assignment of scalar to matrices
// with at least one dynamic index
if (auto* lhs_sub_access = lhs_access->object->As<ast::IndexAccessorExpression>()) {
const auto* lhs_sub_access_type = TypeOf(lhs_sub_access->object);
if (!validate_obj_not_pointer(lhs_sub_access_type)) {
return false;
}
if (auto* mat = lhs_sub_access_type->UnwrapRef()->As<core::type::Matrix>()) {
auto* rhs_row_idx_sem = builder_.Sem().GetVal(lhs_access->index);
auto* rhs_col_idx_sem = builder_.Sem().GetVal(lhs_sub_access->index);
if (!rhs_row_idx_sem->ConstantValue() || !rhs_col_idx_sem->ConstantValue()) {
return EmitDynamicMatrixScalarAssignment(stmt, mat);
}
}
}
// BUG(crbug.com/tint/1333): work around assignment of vector to matrices
// with dynamic indices
const auto* lhs_access_type = TypeOf(lhs_access->object);
if (!validate_obj_not_pointer(lhs_access_type)) {
return false;
}
if (auto* mat = lhs_access_type->UnwrapRef()->As<core::type::Matrix>()) {
auto* lhs_index_sem = builder_.Sem().GetVal(lhs_access->index);
if (!lhs_index_sem->ConstantValue()) {
return EmitDynamicMatrixVectorAssignment(stmt, mat);
}
}
// BUG(crbug.com/tint/534): work around assignment to vectors with dynamic
// indices
if (auto* vec = lhs_access_type->UnwrapRef()->As<core::type::Vector>()) {
auto* rhs_sem = builder_.Sem().GetVal(lhs_access->index);
if (!rhs_sem->ConstantValue()) {
return EmitDynamicVectorAssignment(stmt, vec);
}
}
}
auto out = Line();
if (!EmitExpression(out, stmt->lhs)) {
return false;
}
out << " = ";
if (!EmitExpression(out, stmt->rhs)) {
return false;
}
out << ";";
return true;
}
bool ASTPrinter::EmitBinary(StringStream& out, const ast::BinaryExpression* expr) {
if (expr->op == core::BinaryOp::kLogicalAnd || expr->op == core::BinaryOp::kLogicalOr) {
auto name = UniqueIdentifier(kTempNamePrefix);
{
auto pre = Line();
pre << "bool " << name << " = ";
if (!EmitExpression(pre, expr->lhs)) {
return false;
}
pre << ";";
}
if (expr->op == core::BinaryOp::kLogicalOr) {
Line() << "if (!" << name << ") {";
} else {
Line() << "if (" << name << ") {";
}
{
ScopedIndent si(this);
auto pre = Line();
pre << name << " = ";
if (!EmitExpression(pre, expr->rhs)) {
return false;
}
pre << ";";
}
Line() << "}";
out << "(" << name << ")";
return true;
}
auto* lhs_type = TypeOf(expr->lhs)->UnwrapRef();
auto* rhs_type = TypeOf(expr->rhs)->UnwrapRef();
// Multiplying by a matrix requires the use of `mul` in order to get the
// type of multiply we desire.
if (expr->op == core::BinaryOp::kMultiply &&
((lhs_type->Is<core::type::Vector>() && rhs_type->Is<core::type::Matrix>()) ||
(lhs_type->Is<core::type::Matrix>() && rhs_type->Is<core::type::Vector>()) ||
(lhs_type->Is<core::type::Matrix>() && rhs_type->Is<core::type::Matrix>()))) {
// Matrices are transposed, so swap LHS and RHS.
out << "mul(";
if (!EmitExpression(out, expr->rhs)) {
return false;
}
out << ", ";
if (!EmitExpression(out, expr->lhs)) {
return false;
}
out << ")";
return true;
}
ScopedParen sp(out);
if (!EmitExpression(out, expr->lhs)) {
return false;
}
out << " ";
switch (expr->op) {
case core::BinaryOp::kAnd:
out << "&";
break;
case core::BinaryOp::kOr:
out << "|";
break;
case core::BinaryOp::kXor:
out << "^";
break;
case core::BinaryOp::kLogicalAnd:
case core::BinaryOp::kLogicalOr: {
// These are both handled above.
TINT_UNREACHABLE();
}
case core::BinaryOp::kEqual:
out << "==";
break;
case core::BinaryOp::kNotEqual:
out << "!=";
break;
case core::BinaryOp::kLessThan:
out << "<";
break;
case core::BinaryOp::kGreaterThan:
out << ">";
break;
case core::BinaryOp::kLessThanEqual:
out << "<=";
break;
case core::BinaryOp::kGreaterThanEqual:
out << ">=";
break;
case core::BinaryOp::kShiftLeft:
out << "<<";
break;
case core::BinaryOp::kShiftRight:
// TODO(dsinclair): MSL is based on C++14, and >> in C++14 has
// implementation-defined behaviour for negative LHS. We may have to
// generate extra code to implement WGSL-specified behaviour for negative
// LHS.
out << R"(>>)";
break;
case core::BinaryOp::kAdd:
out << "+";
break;
case core::BinaryOp::kSubtract:
out << "-";
break;
case core::BinaryOp::kMultiply:
out << "*";
break;
case core::BinaryOp::kDivide:
out << "/";
break;
case core::BinaryOp::kModulo:
out << "%";
break;
}
out << " ";
if (!EmitExpression(out, expr->rhs)) {
return false;
}
return true;
}
bool ASTPrinter::EmitStatements(VectorRef<const ast::Statement*> stmts) {
for (auto* s : stmts) {
if (!EmitStatement(s)) {
return false;
}
}
return true;
}
bool ASTPrinter::EmitStatementsWithIndent(VectorRef<const ast::Statement*> stmts) {
ScopedIndent si(this);
return EmitStatements(stmts);
}
bool ASTPrinter::EmitBlock(const ast::BlockStatement* stmt) {
Line() << "{";
if (!EmitStatementsWithIndent(stmt->statements)) {
return false;
}
Line() << "}";
return true;
}
bool ASTPrinter::EmitBreak(const ast::BreakStatement*) {
Line() << "break;";
return true;
}
bool ASTPrinter::EmitBreakIf(const ast::BreakIfStatement* b) {
auto out = Line();
out << "if (";
if (!EmitExpression(out, b->condition)) {
return false;
}
out << ") { break; }";
return true;
}
bool ASTPrinter::EmitCall(StringStream& out, const ast::CallExpression* expr) {
auto* call = builder_.Sem().Get<sem::Call>(expr);
auto* target = call->Target();
return Switch(
target, //
[&](const sem::Function* func) { return EmitFunctionCall(out, call, func); },
[&](const sem::BuiltinFn* builtin) { return EmitBuiltinCall(out, call, builtin); },
[&](const sem::ValueConversion* conv) { return EmitValueConversion(out, call, conv); },
[&](const sem::ValueConstructor* ctor) { return EmitValueConstructor(out, call, ctor); },
TINT_ICE_ON_NO_MATCH);
}
bool ASTPrinter::EmitFunctionCall(StringStream& out,
const sem::Call* call,
const sem::Function* func) {
auto* expr = call->Declaration();
if (ast::HasAttribute<CalculateArrayLength::BufferSizeIntrinsic>(
func->Declaration()->attributes)) {
// Special function generated by the CalculateArrayLength transform for
// calling X.GetDimensions(Y)
if (!EmitExpression(out, call->Arguments()[0]->Declaration())) {
return false;
}
out << ".GetDimensions(";
if (!EmitExpression(out, call->Arguments()[1]->Declaration())) {
return false;
}
out << ")";
return true;
}
if (auto* intrinsic =
ast::GetAttribute<DecomposeMemoryAccess::Intrinsic>(func->Declaration()->attributes)) {
switch (intrinsic->address_space) {
case core::AddressSpace::kUniform:
return EmitUniformBufferAccess(out, expr, intrinsic);
case core::AddressSpace::kStorage:
if (!intrinsic->IsAtomic()) {
return EmitStorageBufferAccess(out, expr, intrinsic);
}
break;
default:
TINT_UNREACHABLE() << "unsupported DecomposeMemoryAccess::Intrinsic address space:"
<< intrinsic->address_space;
}
}
if (auto* wave_intrinsic =
ast::GetAttribute<ast::transform::CanonicalizeEntryPointIO::HLSLWaveIntrinsic>(
func->Declaration()->attributes)) {
switch (wave_intrinsic->op) {
case ast::transform::CanonicalizeEntryPointIO::HLSLWaveIntrinsic::Op::kWaveGetLaneCount:
out << "WaveGetLaneCount()";
return true;
case ast::transform::CanonicalizeEntryPointIO::HLSLWaveIntrinsic::Op::kWaveGetLaneIndex:
out << "WaveGetLaneIndex()";
return true;
}
}
out << func->Declaration()->name->symbol.Name() << "(";
bool first = true;
for (auto* arg : call->Arguments()) {
if (!first) {
out << ", ";
}
first = false;
if (!EmitExpression(out, arg->Declaration())) {
return false;
}
}
out << ")";
return true;
}
bool ASTPrinter::EmitBuiltinCall(StringStream& out,
const sem::Call* call,
const sem::BuiltinFn* builtin) {
const auto type = builtin->Fn();
auto* expr = call->Declaration();
if (builtin->IsTexture()) {
return EmitTextureCall(out, call, builtin);
}
if (type == wgsl::BuiltinFn::kBitcast) {
return EmitBitcastCall(out, expr);
}
if (type == wgsl::BuiltinFn::kSelect) {
return EmitSelectCall(out, expr);
}
if (type == wgsl::BuiltinFn::kModf) {
return EmitModfCall(out, expr, builtin);
}
if (type == wgsl::BuiltinFn::kFrexp) {
return EmitFrexpCall(out, expr, builtin);
}
if (type == wgsl::BuiltinFn::kDegrees) {
return EmitDegreesCall(out, expr, builtin);
}
if (type == wgsl::BuiltinFn::kRadians) {
return EmitRadiansCall(out, expr, builtin);
}
if (type == wgsl::BuiltinFn::kSign) {
return EmitSignCall(out, call, builtin);
}
if (type == wgsl::BuiltinFn::kQuantizeToF16) {
return EmitQuantizeToF16Call(out, expr, builtin);
}
if (type == wgsl::BuiltinFn::kTrunc) {
return EmitTruncCall(out, expr, builtin);
}
if (builtin->IsDataPacking()) {
return EmitDataPackingCall(out, expr, builtin);
}
if (builtin->IsDataUnpacking()) {
return EmitDataUnpackingCall(out, expr, builtin);
}
if (builtin->IsBarrier()) {
return EmitBarrierCall(out, builtin);
}
if (builtin->IsAtomic()) {
return EmitWorkgroupAtomicCall(out, expr, builtin);
}
if (builtin->IsPacked4x8IntegerDotProductBuiltin()) {
return EmitPacked4x8IntegerDotProductBuiltinCall(out, expr, builtin);
}
if (builtin->IsSubgroup()) {
if (builtin->Fn() == wgsl::BuiltinFn::kSubgroupBroadcast) {
// Fall through the regular path.
} else {
return EmitSubgroupCall(out, expr, builtin);
}
}
auto name = generate_builtin_name(builtin);
if (name.empty()) {
return false;
}
// Handle single argument builtins that only accept and return uint (not int overload). We need
// to explicitly cast the return value (we also cast the arg for good measure). See
// crbug.com/tint/1550
if (type == wgsl::BuiltinFn::kCountOneBits || type == wgsl::BuiltinFn::kReverseBits) {
auto* arg = call->Arguments()[0];
if (arg->Type()->UnwrapRef()->is_signed_integer_scalar_or_vector()) {
out << "asint(" << name << "(asuint(";
if (!EmitExpression(out, arg->Declaration())) {
return false;
}
out << ")))";
return true;
}
}
out << name << "(";
bool first = true;
for (auto* arg : call->Arguments()) {
if (!first) {
out << ", ";
}
first = false;
if (!EmitExpression(out, arg->Declaration())) {
return false;
}
}
out << ")";
return true;
}
bool ASTPrinter::EmitValueConversion(StringStream& out,
const sem::Call* call,
const sem::ValueConversion* conv) {
if (!EmitType(out, conv->Target(), core::AddressSpace::kUndefined, core::Access::kReadWrite,
"")) {
return false;
}
out << "(";
if (!EmitExpression(out, call->Arguments()[0]->Declaration())) {
return false;
}
out << ")";
return true;
}
bool ASTPrinter::EmitValueConstructor(StringStream& out,
const sem::Call* call,
const sem::ValueConstructor* ctor) {
auto* type = call->Type();
// If the value constructor arguments are empty then we need to construct with the zero value
// for all components.
if (call->Arguments().IsEmpty()) {
return EmitZeroValue(out, type);
}
// Single parameter matrix initializers must be identity initializer.
// It could also be conversions between f16 and f32 matrix when f16 is properly supported.
if (type->Is<core::type::Matrix>() && call->Arguments().Length() == 1) {
if (!ctor->Parameters()[0]->Type()->UnwrapRef()->is_float_matrix()) {
TINT_UNREACHABLE()
<< "found a single-parameter matrix initializer that is not identity initializer";
}
}
bool brackets = type->IsAnyOf<core::type::Array, core::type::Struct>();
// For single-value vector initializers, swizzle the scalar to the right
// vector dimension using .x
const bool is_single_value_vector_init =
type->is_scalar_vector() && call->Arguments().Length() == 1 &&
ctor->Parameters()[0]->Type()->Is<core::type::Scalar>();
if (brackets) {
out << "{";
} else {
if (!EmitType(out, type, core::AddressSpace::kUndefined, core::Access::kReadWrite, "")) {
return false;
}
out << "(";
}
if (is_single_value_vector_init) {
out << "(";
}
bool first = true;
for (auto* e : call->Arguments()) {
if (!first) {
out << ", ";
}
first = false;
if (!EmitExpression(out, e->Declaration())) {
return false;
}
}
if (is_single_value_vector_init) {
out << ")." << std::string(type->As<core::type::Vector>()->Width(), 'x');
}
out << (brackets ? "}" : ")");
return true;
}
bool ASTPrinter::EmitUniformBufferAccess(StringStream& out,
const ast::CallExpression* expr,
const DecomposeMemoryAccess::Intrinsic* intrinsic) {
auto const buffer = intrinsic->Buffer()->identifier->symbol.Name();
auto* const offset = expr->args[0];
// offset in bytes
uint32_t scalar_offset_bytes = 0;
// offset in uint (4 bytes)
uint32_t scalar_offset_index = 0;
// expression to calculate offset in bytes
std::string scalar_offset_bytes_expr;
// expression to calculate offset in uint, by dividing scalar_offset_bytes_expr by 4
std::string scalar_offset_index_expr;
// expression to calculate offset in uint, independently
std::string scalar_offset_index_unified_expr;
// If true, use scalar_offset_index, otherwise use scalar_offset_index_expr
bool scalar_offset_constant = false;
if (auto* val = builder_.Sem().GetVal(offset)->ConstantValue()) {
TINT_ASSERT(val->Type()->Is<core::type::U32>());
scalar_offset_bytes = static_cast<uint32_t>(val->ValueAs<AInt>());
scalar_offset_index = scalar_offset_bytes / 4; // bytes -> scalar index
scalar_offset_constant = true;
}
// If true, scalar_offset_bytes or scalar_offset_bytes_expr should be used, otherwise only use
// scalar_offset_index or scalar_offset_index_unified_expr. Currently only loading f16 scalar
// require using offset in bytes.
const bool need_offset_in_bytes =
intrinsic->type == DecomposeMemoryAccess::Intrinsic::DataType::kF16;
if (!scalar_offset_constant) {
// UBO offset not compile-time known.
// Calculate the scalar offset into a temporary.
if (need_offset_in_bytes) {
scalar_offset_bytes_expr = UniqueIdentifier("scalar_offset_bytes");
scalar_offset_index_expr = UniqueIdentifier("scalar_offset_index");
{
auto pre = Line();
pre << "const uint " << scalar_offset_bytes_expr << " = (";
if (!EmitExpression(pre, offset)) {
return false;
}
pre << ");";
}
Line() << "const uint " << scalar_offset_index_expr << " = " << scalar_offset_bytes_expr
<< " / 4;";
} else {
scalar_offset_index_unified_expr = UniqueIdentifier("scalar_offset");
auto pre = Line();
pre << "const uint " << scalar_offset_index_unified_expr << " = (";
if (!EmitExpression(pre, offset)) {
return false;
}
pre << ") / 4;";
}
}
const char swizzle[] = {'x', 'y', 'z', 'w'};
using Op = DecomposeMemoryAccess::Intrinsic::Op;
using DataType = DecomposeMemoryAccess::Intrinsic::DataType;
switch (intrinsic->op) {
case Op::kLoad: {
auto cast = [&](const char* to, auto&& load) {
out << to << "(";
auto result = load();
out << ")";
return result;
};
auto load_u32_to = [&](StringStream& target) {
target << buffer;
if (scalar_offset_constant) {
target << "[" << (scalar_offset_index / 4) << "]."
<< swizzle[scalar_offset_index & 3];
} else {
target << "[" << scalar_offset_index_unified_expr << " / 4]["
<< scalar_offset_index_unified_expr << " % 4]";
}
return true;
};
auto load_u32 = [&] { return load_u32_to(out); };
// Has a minimum alignment of 8 bytes, so is either .xy or .zw
auto load_vec2_u32_to = [&](StringStream& target) {
if (scalar_offset_constant) {
target << buffer << "[" << (scalar_offset_index / 4) << "]"
<< ((scalar_offset_index & 2) == 0 ? ".xy" : ".zw");
} else {
std::string ubo_load = UniqueIdentifier("ubo_load");
{
auto pre = Line();
pre << "uint4 " << ubo_load << " = " << buffer << "["
<< scalar_offset_index_unified_expr << " / 4];";
}
target << "((" << scalar_offset_index_unified_expr << " & 2) ? " << ubo_load
<< ".zw : " << ubo_load << ".xy)";
}
return true;
};
auto load_vec2_u32 = [&] { return load_vec2_u32_to(out); };
// vec4 has a minimum alignment of 16 bytes, easiest case
auto load_vec4_u32 = [&] {
out << buffer;
if (scalar_offset_constant) {
out << "[" << (scalar_offset_index / 4) << "]";
} else {
out << "[" << scalar_offset_index_unified_expr << " / 4]";
}
return true;
};
// vec3 has a minimum alignment of 16 bytes, so is just a .xyz swizzle
auto load_vec3_u32 = [&] {
if (!load_vec4_u32()) {
return false;
}
out << ".xyz";
return true;
};
auto load_scalar_f16 = [&] {
// offset bytes = 4k, ((buffer[index].x) & 0xFFFF)
// offset bytes = 4k+2, ((buffer[index].x >> 16) & 0xFFFF)
out << "float16_t(f16tof32(((" << buffer;
if (scalar_offset_constant) {
out << "[" << (scalar_offset_index / 4) << "]."
<< swizzle[scalar_offset_index & 3];
// WGSL spec ensure little endian memory layout.
if (scalar_offset_bytes % 4 == 0) {
out << ") & 0xFFFF)";
} else {
out << " >> 16) & 0xFFFF)";
}
} else {
out << "[" << scalar_offset_index_expr << " / 4][" << scalar_offset_index_expr
<< " % 4] >> (" << scalar_offset_bytes_expr
<< " % 4 == 0 ? 0 : 16)) & 0xFFFF)";
}
out << "))";
return true;
};
auto load_vec2_f16 = [&] {
// vec2<f16> is aligned to 4 bytes
// Preclude code load the vec2<f16> data as a uint:
// uint ubo_load = buffer[id0][id1];
// Loading code convert it to vec2<f16>:
// vector<float16_t, 2>(float16_t(f16tof32(ubo_load & 0xFFFF)),
// float16_t(f16tof32(ubo_load >> 16)))
std::string ubo_load = UniqueIdentifier("ubo_load");
{
auto pre = Line();
// Load the 4 bytes f16 vector as an uint
pre << "uint " << ubo_load << " = ";
if (!load_u32_to(pre)) {
return false;
}
pre << ";";
}
out << "vector<float16_t, 2>(float16_t(f16tof32(" << ubo_load
<< " & 0xFFFF)), float16_t(f16tof32(" << ubo_load << " >> 16)))";
return true;
};
auto load_vec3_f16 = [&] {
// vec3<f16> is aligned to 8 bytes
// Preclude code load the vec3<f16> data as uint2 and convert its elements to
// float16_t:
// uint2 ubo_load = buffer[id0].xy;
// /* The low 8 bits of two uint are the x and z elements of vec3<f16> */
// vector<float16_t> ubo_load_xz = vector<float16_t, 2>(f16tof32(ubo_load &
// 0xFFFF));
// /* The high 8 bits of first uint is the y element of vec3<f16> */
// float16_t ubo_load_y = f16tof32(ubo_load[0] >> 16);
// Loading code convert it to vec3<f16>:
// vector<float16_t, 3>(ubo_load_xz[0], ubo_load_y, ubo_load_xz[1])
std::string ubo_load = UniqueIdentifier("ubo_load");
std::string ubo_load_xz = UniqueIdentifier(ubo_load + "_xz");
std::string ubo_load_y = UniqueIdentifier(ubo_load + "_y");
{
auto pre = Line();
// Load the 8 bytes uint2 with the f16 vector at lower 6 bytes
pre << "uint2 " << ubo_load << " = ";
if (!load_vec2_u32_to(pre)) {
return false;
}
pre << ";";
}
{
auto pre = Line();
pre << "vector<float16_t, 2> " << ubo_load_xz
<< " = vector<float16_t, 2>(f16tof32(" << ubo_load << " & 0xFFFF));";
}
{
auto pre = Line();
pre << "float16_t " << ubo_load_y << " = f16tof32(" << ubo_load
<< "[0] >> 16);";
}
out << "vector<float16_t, 3>(" << ubo_load_xz << "[0], " << ubo_load_y << ", "
<< ubo_load_xz << "[1])";
return true;
};
auto load_vec4_f16 = [&] {
// vec4<f16> is aligned to 8 bytes
// Preclude code load the vec4<f16> data as uint2 and convert its elements to
// float16_t:
// uint2 ubo_load = buffer[id0].xy;
// /* The low 8 bits of two uint are the x and z elements of vec4<f16> */
// vector<float16_t> ubo_load_xz = vector<float16_t, 2>(f16tof32(ubo_load &
// 0xFFFF));
// /* The high 8 bits of two uint are the y and w elements of vec4<f16> */
// vector<float16_t, 2> ubo_load_yw = vector<float16_t, 2>(f16tof32(ubo_load >>
// 16));
// Loading code convert it to vec4<f16>:
// vector<float16_t, 4>(ubo_load_xz[0], ubo_load_yw[0], ubo_load_xz[1],
// ubo_load_yw[1])
std::string ubo_load = UniqueIdentifier("ubo_load");
std::string ubo_load_xz = UniqueIdentifier(ubo_load + "_xz");
std::string ubo_load_yw = UniqueIdentifier(ubo_load + "_yw");
{
auto pre = Line();
// Load the 8 bytes f16 vector as an uint2
pre << "uint2 " << ubo_load << " = ";
if (!load_vec2_u32_to(pre)) {
return false;
}
pre << ";";
}
{
auto pre = Line();
pre << "vector<float16_t, 2> " << ubo_load_xz
<< " = vector<float16_t, 2>(f16tof32(" << ubo_load << " & 0xFFFF));";
}
{
auto pre = Line();
pre << "vector<float16_t, 2> " << ubo_load_yw
<< " = vector<float16_t, 2>(f16tof32(" << ubo_load << " >> 16));";
}
out << "vector<float16_t, 4>(" << ubo_load_xz << "[0], " << ubo_load_yw << "[0], "
<< ubo_load_xz << "[1], " << ubo_load_yw << "[1])";
return true;
};
switch (intrinsic->type) {
case DataType::kU32:
return load_u32();
case DataType::kF32:
return cast("asfloat", load_u32);
case DataType::kI32:
return cast("asint", load_u32);
case DataType::kF16:
return load_scalar_f16();
case DataType::kVec2U32:
return load_vec2_u32();
case DataType::kVec2F32:
return cast("asfloat", load_vec2_u32);
case DataType::kVec2I32:
return cast("asint", load_vec2_u32);
case DataType::kVec2F16:
return load_vec2_f16();
case DataType::kVec3U32:
return load_vec3_u32();
case DataType::kVec3F32:
return cast("asfloat", load_vec3_u32);
case DataType::kVec3I32:
return cast("asint", load_vec3_u32);
case DataType::kVec3F16:
return load_vec3_f16();
case DataType::kVec4U32:
return load_vec4_u32();
case DataType::kVec4F32:
return cast("asfloat", load_vec4_u32);
case DataType::kVec4I32:
return cast("asint", load_vec4_u32);
case DataType::kVec4F16:
return load_vec4_f16();
}
TINT_UNREACHABLE() << "unsupported DecomposeMemoryAccess::Intrinsic::DataType: "
<< static_cast<int>(intrinsic->type);
}
default:
break;
}
TINT_UNREACHABLE() << "unsupported DecomposeMemoryAccess::Intrinsic::Op: "
<< static_cast<int>(intrinsic->op);
}
bool ASTPrinter::EmitStorageBufferAccess(StringStream& out,
const ast::CallExpression* expr,
const DecomposeMemoryAccess::Intrinsic* intrinsic) {
auto const buffer = intrinsic->Buffer()->identifier->symbol.Name();
auto* const offset = expr->args[0];
auto* const value = expr->args.Length() > 1 ? expr->args[1] : nullptr;
using Op = DecomposeMemoryAccess::Intrinsic::Op;
using DataType = DecomposeMemoryAccess::Intrinsic::DataType;
switch (intrinsic->op) {
case Op::kLoad: {
auto load = [&](const char* cast, int n) {
if (cast) {
out << cast << "(";
}
out << buffer << ".Load";
if (n > 1) {
out << n;
}
ScopedParen sp(out);
if (!EmitExpression(out, offset)) {
return false;
}
if (cast) {
out << ")";
}
return true;
};
// Templated load used for f16 types, requires SM6.2 or higher and DXC
// Used by loading f16 types, e.g. for f16 type, set type parameter to "float16_t"
// to emit `buffer.Load<float16_t>(offset)`.
auto templated_load = [&](const char* type) {
out << buffer << ".Load<" << type << ">"; // templated load
ScopedParen sp(out);
if (!EmitExpression(out, offset)) {
return false;
}
return true;
};
switch (intrinsic->type) {
case DataType::kU32:
return load(nullptr, 1);
case DataType::kF32:
return load("asfloat", 1);
case DataType::kI32:
return load("asint", 1);
case DataType::kF16:
return templated_load("float16_t");
case DataType::kVec2U32:
return load(nullptr, 2);
case DataType::kVec2F32:
return load("asfloat", 2);
case DataType::kVec2I32:
return load("asint", 2);
case DataType::kVec2F16:
return templated_load("vector<float16_t, 2> ");
case DataType::kVec3U32:
return load(nullptr, 3);
case DataType::kVec3F32:
return load("asfloat", 3);
case DataType::kVec3I32:
return load("asint", 3);
case DataType::kVec3F16:
return templated_load("vector<float16_t, 3> ");
case DataType::kVec4U32:
return load(nullptr, 4);
case DataType::kVec4F32:
return load("asfloat", 4);
case DataType::kVec4I32:
return load("asint", 4);
case DataType::kVec4F16:
return templated_load("vector<float16_t, 4> ");
}
TINT_UNREACHABLE() << "unsupported DecomposeMemoryAccess::Intrinsic::DataType: "
<< static_cast<int>(intrinsic->type);
}
case Op::kStore: {
auto store = [&](int n) {
out << buffer << ".Store";
if (n > 1) {
out << n;
}
ScopedParen sp1(out);
if (!EmitExpression(out, offset)) {
return false;
}
out << ", asuint";
ScopedParen sp2(out);
if (!EmitTextureOrStorageBufferCallArgExpression(out, value)) {
return false;
}
return true;
};
// Templated stored used for f16 types, requires SM6.2 or higher and DXC
// Used by storing f16 types, e.g. for f16 type, set type parameter to "float16_t"
// to emit `buffer.Store<float16_t>(offset)`.
auto templated_store = [&](const char* type) {
out << buffer << ".Store<" << type << ">"; // templated store
ScopedParen sp1(out);
if (!EmitExpression(out, offset)) {
return false;
}
out << ", ";
if (!EmitExpression(out, value)) {
return false;
}
return true;
};
switch (intrinsic->type) {
case DataType::kU32:
return store(1);
case DataType::kF32:
return store(1);
case DataType::kI32:
return store(1);
case DataType::kF16:
return templated_store("float16_t");
case DataType::kVec2U32:
return store(2);
case DataType::kVec2F32:
return store(2);
case DataType::kVec2I32:
return store(2);
case DataType::kVec2F16:
return templated_store("vector<float16_t, 2> ");
case DataType::kVec3U32:
return store(3);
case DataType::kVec3F32:
return store(3);
case DataType::kVec3I32:
return store(3);
case DataType::kVec3F16:
return templated_store("vector<float16_t, 3> ");
case DataType::kVec4U32:
return store(4);
case DataType::kVec4F32:
return store(4);
case DataType::kVec4I32:
return store(4);
case DataType::kVec4F16:
return templated_store("vector<float16_t, 4> ");
}
TINT_UNREACHABLE() << "unsupported DecomposeMemoryAccess::Intrinsic::DataType: "
<< static_cast<int>(intrinsic->type);
}
default:
// Break out to error case below
// Note that atomic intrinsics are generated as functions.
break;
}
TINT_UNREACHABLE() << "unsupported DecomposeMemoryAccess::Intrinsic::Op: "
<< static_cast<int>(intrinsic->op);
}
bool ASTPrinter::EmitStorageAtomicIntrinsic(const ast::Function* func,
const DecomposeMemoryAccess::Intrinsic* intrinsic) {
using Op = DecomposeMemoryAccess::Intrinsic::Op;
const sem::Function* sem_func = builder_.Sem().Get(func);
auto* result_ty = sem_func->ReturnType();
const auto name = func->name->symbol.Name();
auto& buf = *current_buffer_;
auto const buffer = intrinsic->Buffer()->identifier->symbol.Name();
auto rmw = [&](const char* hlsl) -> bool {
{
auto fn = Line(&buf);
if (!EmitTypeAndName(fn, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, name)) {
return false;
}
fn << "(uint offset, ";
if (!EmitTypeAndName(fn, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "value")) {
return false;
}
fn << ") {";
}
buf.IncrementIndent();
TINT_DEFER({
buf.DecrementIndent();
Line(&buf) << "}";
Line(&buf);
});
{
auto l = Line(&buf);
if (!EmitTypeAndName(l, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "original_value")) {
return false;
}
l << " = 0;";
}
{
auto l = Line(&buf);
l << buffer << "." << hlsl << "(offset, ";
if (intrinsic->op == Op::kAtomicSub) {
l << "-";
}
l << "value, original_value);";
}
Line(&buf) << "return original_value;";
return true;
};
switch (intrinsic->op) {
case Op::kAtomicAdd:
return rmw("InterlockedAdd");
case Op::kAtomicSub:
// Use add with the operand negated.
return rmw("InterlockedAdd");
case Op::kAtomicMax:
return rmw("InterlockedMax");
case Op::kAtomicMin:
return rmw("InterlockedMin");
case Op::kAtomicAnd:
return rmw("InterlockedAnd");
case Op::kAtomicOr:
return rmw("InterlockedOr");
case Op::kAtomicXor:
return rmw("InterlockedXor");
case Op::kAtomicExchange:
return rmw("InterlockedExchange");
case Op::kAtomicLoad: {
// HLSL does not have an InterlockedLoad, so we emulate it with
// InterlockedOr using 0 as the OR value
{
auto fn = Line(&buf);
if (!EmitTypeAndName(fn, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, name)) {
return false;
}
fn << "(uint offset) {";
}
buf.IncrementIndent();
TINT_DEFER({
buf.DecrementIndent();
Line(&buf) << "}";
Line(&buf);
});
{
auto l = Line(&buf);
if (!EmitTypeAndName(l, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "value")) {
return false;
}
l << " = 0;";
}
Line(&buf) << buffer << ".InterlockedOr(offset, 0, value);";
Line(&buf) << "return value;";
return true;
}
case Op::kAtomicStore: {
auto* const value_ty = sem_func->Parameters()[1]->Type()->UnwrapRef();
// HLSL does not have an InterlockedStore, so we emulate it with
// InterlockedExchange and discard the returned value
{
auto fn = Line(&buf);
fn << "void " << name << "(uint offset, ";
if (!EmitTypeAndName(fn, value_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "value")) {
return false;
}
fn << ") {";
}
buf.IncrementIndent();
TINT_DEFER({
buf.DecrementIndent();
Line(&buf) << "}";
Line(&buf);
});
{
auto l = Line(&buf);
if (!EmitTypeAndName(l, value_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "ignored")) {
return false;
}
l << ";";
}
Line(&buf) << buffer << ".InterlockedExchange(offset, value, ignored);";
return true;
}
case Op::kAtomicCompareExchangeWeak: {
if (!EmitStructType(&helpers_, result_ty->As<core::type::Struct>())) {
return false;
}
auto* const value_ty = sem_func->Parameters()[1]->Type()->UnwrapRef();
// NOTE: We don't need to emit the return type struct here as DecomposeMemoryAccess
// already added it to the AST, and it should have already been emitted by now.
{
auto fn = Line(&buf);
if (!EmitTypeAndName(fn, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, name)) {
return false;
}
fn << "(uint offset, ";
if (!EmitTypeAndName(fn, value_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "compare")) {
return false;
}
fn << ", ";
if (!EmitTypeAndName(fn, value_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "value")) {
return false;
}
fn << ") {";
}
buf.IncrementIndent();
TINT_DEFER({
buf.DecrementIndent();
Line(&buf) << "}";
Line(&buf);
});
{ // T result = {0};
auto l = Line(&buf);
if (!EmitTypeAndName(l, result_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, "result")) {
return false;
}
l << "=";
if (!EmitZeroValue(l, result_ty)) {
return false;
}
l << ";";
}
Line(&buf) << buffer
<< ".InterlockedCompareExchange(offset, compare, value, result.old_value);";
Line(&buf) << "result.exchanged = result.old_value == compare;";
Line(&buf) << "return result;";
return true;
}
default:
break;
}
TINT_UNREACHABLE() << "unsupported atomic DecomposeMemoryAccess::Intrinsic::Op: "
<< static_cast<int>(intrinsic->op);
}
bool ASTPrinter::EmitWorkgroupAtomicCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
std::string result = UniqueIdentifier("atomic_result");
if (!builtin->ReturnType()->Is<core::type::Void>()) {
auto pre = Line();
if (!EmitTypeAndName(pre, builtin->ReturnType(), core::AddressSpace::kUndefined,
core::Access::kUndefined, result)) {
return false;
}
pre << " = ";
if (!EmitZeroValue(pre, builtin->ReturnType())) {
return false;
}
pre << ";";
}
auto call = [&](const char* name) {
auto pre = Line();
pre << name;
{
ScopedParen sp(pre);
for (size_t i = 0; i < expr->args.Length(); i++) {
auto* arg = expr->args[i];
if (i > 0) {
pre << ", ";
}
if (!EmitExpression(pre, arg)) {
return false;
}
}
pre << ", " << result;
}
pre << ";";
out << result;
return true;
};
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kAtomicLoad: {
// HLSL does not have an InterlockedLoad, so we emulate it with
// InterlockedOr using 0 as the OR value
auto pre = Line();
pre << "InterlockedOr";
{
ScopedParen sp(pre);
if (!EmitExpression(pre, expr->args[0])) {
return false;
}
pre << ", 0, " << result;
}
pre << ";";
out << result;
return true;
}
case wgsl::BuiltinFn::kAtomicStore: {
// HLSL does not have an InterlockedStore, so we emulate it with
// InterlockedExchange and discard the returned value
{ // T result = 0;
auto pre = Line();
auto* value_ty = builtin->Parameters()[1]->Type()->UnwrapRef();
if (!EmitTypeAndName(pre, value_ty, core::AddressSpace::kUndefined,
core::Access::kUndefined, result)) {
return false;
}
pre << " = ";
if (!EmitZeroValue(pre, value_ty)) {
return false;
}
pre << ";";
}
out << "InterlockedExchange";
{
ScopedParen sp(out);
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << ", ";
if (!EmitExpression(out, expr->args[1])) {
return false;
}
out << ", " << result;
}
return true;
}
case wgsl::BuiltinFn::kAtomicCompareExchangeWeak: {
if (!EmitStructType(&helpers_, builtin->ReturnType()->As<core::type::Struct>())) {
return false;
}
auto* dest = expr->args[0];
auto* compare_value = expr->args[1];
auto* value = expr->args[2];
std::string compare = UniqueIdentifier("atomic_compare_value");
{ // T compare_value = <compare_value>;
auto pre = Line();
if (!EmitTypeAndName(pre, TypeOf(compare_value)->UnwrapRef(),
core::AddressSpace::kUndefined, core::Access::kUndefined,
compare)) {
return false;
}
pre << " = ";
if (!EmitExpression(pre, compare_value)) {
return false;
}
pre << ";";
}
{ // InterlockedCompareExchange(dst, compare, value, result.old_value);
auto pre = Line();
pre << "InterlockedCompareExchange";
{
ScopedParen sp(pre);
if (!EmitExpression(pre, dest)) {
return false;
}
pre << ", " << compare << ", ";
if (!EmitExpression(pre, value)) {
return false;
}
pre << ", " << result << ".old_value";
}
pre << ";";
}
// result.exchanged = result.old_value == compare;
Line() << result << ".exchanged = " << result << ".old_value == " << compare << ";";
out << result;
return true;
}
case wgsl::BuiltinFn::kAtomicAdd:
return call("InterlockedAdd");
case wgsl::BuiltinFn::kAtomicSub: {
auto pre = Line();
// Sub uses InterlockedAdd with the operand negated.
pre << "InterlockedAdd";
{
ScopedParen sp(pre);
TINT_ASSERT(expr->args.Length() == 2);
if (!EmitExpression(pre, expr->args[0])) {
return false;
}
pre << ", -";
{
ScopedParen argSP(pre);
if (!EmitExpression(pre, expr->args[1])) {
return false;
}
}
pre << ", " << result;
}
pre << ";";
out << result;
}
return true;
case wgsl::BuiltinFn::kAtomicMax:
return call("InterlockedMax");
case wgsl::BuiltinFn::kAtomicMin:
return call("InterlockedMin");
case wgsl::BuiltinFn::kAtomicAnd:
return call("InterlockedAnd");
case wgsl::BuiltinFn::kAtomicOr:
return call("InterlockedOr");
case wgsl::BuiltinFn::kAtomicXor:
return call("InterlockedXor");
case wgsl::BuiltinFn::kAtomicExchange:
return call("InterlockedExchange");
default:
break;
}
TINT_UNREACHABLE() << "unsupported atomic builtin: " << builtin->Fn();
}
bool ASTPrinter::EmitSelectCall(StringStream& out, const ast::CallExpression* expr) {
auto* expr_false = expr->args[0];
auto* expr_true = expr->args[1];
auto* expr_cond = expr->args[2];
ScopedParen paren(out);
if (!EmitExpression(out, expr_cond)) {
return false;
}
out << " ? ";
if (!EmitExpression(out, expr_true)) {
return false;
}
out << " : ";
if (!EmitExpression(out, expr_false)) {
return false;
}
return true;
}
bool ASTPrinter::EmitModfCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
return CallBuiltinHelper(
out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
auto* ty = builtin->Parameters()[0]->Type();
auto in = params[0];
std::string width;
if (auto* vec = ty->As<core::type::Vector>()) {
width = std::to_string(vec->Width());
}
// Emit the builtin return type unique to this overload. This does not
// exist in the AST, so it will not be generated in Generate().
if (!EmitStructType(&helpers_, builtin->ReturnType()->As<core::type::Struct>())) {
return false;
}
{
auto l = Line(b);
if (!EmitType(l, builtin->ReturnType(), core::AddressSpace::kUndefined,
core::Access::kUndefined, "")) {
return false;
}
l << " result;";
}
Line(b) << "result.fract = modf(" << params[0] << ", result.whole);";
Line(b) << "return result;";
return true;
});
}
bool ASTPrinter::EmitFrexpCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
return CallBuiltinHelper(
out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
auto* ty = builtin->Parameters()[0]->Type();
auto in = params[0];
std::string width;
if (auto* vec = ty->As<core::type::Vector>()) {
width = std::to_string(vec->Width());
}
// Emit the builtin return type unique to this overload. This does not
// exist in the AST, so it will not be generated in Generate().
if (!EmitStructType(&helpers_, builtin->ReturnType()->As<core::type::Struct>())) {
return false;
}
std::string member_type;
if (Is<core::type::F16>(ty->DeepestElement())) {
member_type = width.empty() ? "float16_t" : ("vector<float16_t, " + width + ">");
} else {
member_type = "float" + width;
}
Line(b) << member_type << " exp;";
Line(b) << member_type << " fract = sign(" << in << ") * frexp(" << in << ", exp);";
{
auto l = Line(b);
if (!EmitType(l, builtin->ReturnType(), core::AddressSpace::kUndefined,
core::Access::kUndefined, "")) {
return false;
}
l << " result = {fract, int" << width << "(exp)};";
}
Line(b) << "return result;";
return true;
});
}
bool ASTPrinter::EmitDegreesCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
return CallBuiltinHelper(out, expr, builtin,
[&](TextBuffer* b, const std::vector<std::string>& params) {
Line(b) << "return " << params[0] << " * " << std::setprecision(20)
<< sem::kRadToDeg << ";";
return true;
});
}
bool ASTPrinter::EmitRadiansCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
return CallBuiltinHelper(out, expr, builtin,
[&](TextBuffer* b, const std::vector<std::string>& params) {
Line(b) << "return " << params[0] << " * " << std::setprecision(20)
<< sem::kDegToRad << ";";
return true;
});
}
// The HLSL `sign` method always returns an `int` result (scalar or vector). In WGSL the result is
// expected to be the same type as the argument. This injects a cast to the expected WGSL result
// type after the call to `sign`.
bool ASTPrinter::EmitSignCall(StringStream& out, const sem::Call* call, const sem::BuiltinFn*) {
auto* arg = call->Arguments()[0];
if (!EmitType(out, arg->Type(), core::AddressSpace::kUndefined, core::Access::kReadWrite, "")) {
return false;
}
out << "(sign(";
if (!EmitExpression(out, arg->Declaration())) {
return false;
}
out << "))";
return true;
}
bool ASTPrinter::EmitQuantizeToF16Call(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
// Cast to f16 and back
std::string width;
if (auto* vec = builtin->ReturnType()->As<core::type::Vector>()) {
width = std::to_string(vec->Width());
}
out << "f16tof32(f32tof16" << "(";
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << "))";
return true;
}
bool ASTPrinter::EmitTruncCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
// HLSL's trunc is broken for very large/small float values.
// See crbug.com/tint/1883
return CallBuiltinHelper( //
out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
// value < 0 ? ceil(value) : floor(value)
Line(b) << "return " << params[0] << " < 0 ? ceil(" << params[0] << ") : floor("
<< params[0] << ");";
return true;
});
}
bool ASTPrinter::EmitDataPackingCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
return CallBuiltinHelper(
out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
uint32_t dims = 2;
bool is_signed = false;
uint32_t scale = 65535;
if (builtin->Fn() == wgsl::BuiltinFn::kPack4X8Snorm ||
builtin->Fn() == wgsl::BuiltinFn::kPack4X8Unorm) {
dims = 4;
scale = 255;
}
if (builtin->Fn() == wgsl::BuiltinFn::kPack4X8Snorm ||
builtin->Fn() == wgsl::BuiltinFn::kPack2X16Snorm) {
is_signed = true;
scale = (scale - 1) / 2;
}
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kPack4X8Snorm:
case wgsl::BuiltinFn::kPack4X8Unorm:
case wgsl::BuiltinFn::kPack2X16Snorm:
case wgsl::BuiltinFn::kPack2X16Unorm: {
{
auto l = Line(b);
l << (is_signed ? "" : "u") << "int" << dims
<< " i = " << (is_signed ? "" : "u") << "int" << dims << "(round(clamp("
<< params[0] << ", " << (is_signed ? "-1.0" : "0.0") << ", 1.0) * "
<< scale << ".0))";
if (is_signed) {
l << " & " << (dims == 4 ? "0xff" : "0xffff");
}
l << ";";
}
{
auto l = Line(b);
l << "return ";
if (is_signed) {
l << "asuint";
}
l << "(i.x | i.y << " << (32 / dims);
if (dims == 4) {
l << " | i.z << 16 | i.w << 24";
}
l << ");";
}
break;
}
case wgsl::BuiltinFn::kPack2X16Float: {
Line(b) << "uint2 i = f32tof16(" << params[0] << ");";
Line(b) << "return i.x | (i.y << 16);";
break;
}
default:
TINT_ICE() << " unhandled data packing builtin";
}
return true;
});
}
bool ASTPrinter::EmitDataUnpackingCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
return CallBuiltinHelper(
out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
uint32_t dims = 2;
bool is_signed = false;
uint32_t scale = 65535;
if (builtin->Fn() == wgsl::BuiltinFn::kUnpack4X8Snorm ||
builtin->Fn() == wgsl::BuiltinFn::kUnpack4X8Unorm) {
dims = 4;
scale = 255;
}
if (builtin->Fn() == wgsl::BuiltinFn::kUnpack4X8Snorm ||
builtin->Fn() == wgsl::BuiltinFn::kUnpack2X16Snorm) {
is_signed = true;
scale = (scale - 1) / 2;
}
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kUnpack4X8Snorm:
case wgsl::BuiltinFn::kUnpack2X16Snorm: {
Line(b) << "int j = int(" << params[0] << ");";
{ // Perform sign extension on the converted values.
auto l = Line(b);
l << "int" << dims << " i = int" << dims << "(";
if (dims == 2) {
l << "j << 16, j) >> 16";
} else {
l << "j << 24, j << 16, j << 8, j) >> 24";
}
l << ";";
}
Line(b) << "return clamp(float" << dims << "(i) / " << scale << ".0, "
<< (is_signed ? "-1.0" : "0.0") << ", 1.0);";
break;
}
case wgsl::BuiltinFn::kUnpack4X8Unorm:
case wgsl::BuiltinFn::kUnpack2X16Unorm: {
Line(b) << "uint j = " << params[0] << ";";
{
auto l = Line(b);
l << "uint" << dims << " i = uint" << dims << "(";
l << "j & " << (dims == 2 ? "0xffff" : "0xff") << ", ";
if (dims == 4) {
l << "(j >> " << (32 / dims) << ") & 0xff, (j >> 16) & 0xff, j >> 24";
} else {
l << "j >> " << (32 / dims);
}
l << ");";
}
Line(b) << "return float" << dims << "(i) / " << scale << ".0;";
break;
}
case wgsl::BuiltinFn::kUnpack2X16Float:
Line(b) << "uint i = " << params[0] << ";";
Line(b) << "return f16tof32(uint2(i & 0xffff, i >> 16));";
break;
default:
TINT_ICE() << "unhandled data packing builtin";
}
return true;
});
}
bool ASTPrinter::EmitPacked4x8IntegerDotProductBuiltinCall(StringStream& out,
const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kDot4I8Packed:
case wgsl::BuiltinFn::kDot4U8Packed:
break;
case wgsl::BuiltinFn::kPack4XI8: {
out << "uint(pack_s8(";
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << "))";
return true;
}
case wgsl::BuiltinFn::kPack4XU8: {
out << "uint(pack_u8(";
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << "))";
return true;
}
case wgsl::BuiltinFn::kPack4XI8Clamp: {
out << "uint(pack_clamp_s8(";
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << "))";
return true;
}
case wgsl::BuiltinFn::kUnpack4XI8: {
out << "unpack_s8s32(int8_t4_packed(";
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << "))";
return true;
}
case wgsl::BuiltinFn::kUnpack4XU8: {
out << "unpack_u8u32(uint8_t4_packed(";
if (!EmitExpression(out, expr->args[0])) {
return false;
}
out << "))";
return true;
}
case wgsl::BuiltinFn::kPack4XU8Clamp:
default:
TINT_UNIMPLEMENTED() << builtin->Fn();
}
return CallBuiltinHelper(out, expr, builtin,
[&](TextBuffer* b, const std::vector<std::string>& params) {
std::string functionName;
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kDot4I8Packed:
Line(b) << "int accumulator = 0;";
functionName = "dot4add_i8packed";
break;
case wgsl::BuiltinFn::kDot4U8Packed:
Line(b) << "uint accumulator = 0u;";
functionName = "dot4add_u8packed";
break;
default:
TINT_ICE() << "Internal error: unhandled DP4a builtin";
}
Line(b) << "return " << functionName << "(" << params[0] << ", "
<< params[1] << ", accumulator);";
return true;
});
}
bool ASTPrinter::EmitBarrierCall(StringStream& out, const sem::BuiltinFn* builtin) {
// TODO(crbug.com/tint/661): Combine sequential barriers to a single
// instruction.
if (builtin->Fn() == wgsl::BuiltinFn::kWorkgroupBarrier) {
out << "GroupMemoryBarrierWithGroupSync()";
} else if (builtin->Fn() == wgsl::BuiltinFn::kStorageBarrier) {
out << "DeviceMemoryBarrierWithGroupSync()";
} else if (builtin->Fn() == wgsl::BuiltinFn::kTextureBarrier) {
out << "DeviceMemoryBarrierWithGroupSync()";
} else {
TINT_UNREACHABLE() << "unexpected barrier builtin type " << builtin->Fn();
}
return true;
}
bool ASTPrinter::EmitSubgroupCall(StringStream& out,
[[maybe_unused]] const ast::CallExpression* expr,
const sem::BuiltinFn* builtin) {
if (builtin->Fn() == wgsl::BuiltinFn::kSubgroupBallot) {
out << "WaveActiveBallot(true)";
} else {
// subgroupBroadcast is already handled in the regular builtin flow.
TINT_UNREACHABLE() << "unexpected subgroup builtin type " << builtin->Fn();
}
return true;
}
bool ASTPrinter::EmitTextureOrStorageBufferCallArgExpression(StringStream& out,
const ast::Expression* expr) {
// TODO(crbug.com/tint/1976): Workaround DXC bug that fails to compile texture/storage function
// calls with signed integer splatted constants. DXC fails to convert the coord arg, for e.g.
// `0.xxx`, from a vector of 64-bit ints to a vector of 32-bit ints to match the texture load
// parameter type. We work around this for now by explicitly casting the splatted constant to
// the right type, for e.g. `int3(0.xxx)`.
bool emitted_cast = false;
if (auto* sem = builder_.Sem().GetVal(expr)) {
if (auto* constant = sem->ConstantValue()) {
if (auto* splat = constant->As<core::constant::Splat>()) {
if (splat->Type()->is_signed_integer_vector()) {
if (!EmitType(out, constant->Type(), core::AddressSpace::kUndefined,
core::Access::kUndefined, "")) {
return false;
}
out << "(";
emitted_cast = true;
}
}
}
}
if (!EmitExpression(out, expr)) {
return false;
}
if (emitted_cast) {
out << ")";
}
return true;
}
bool ASTPrinter::EmitTextureCall(StringStream& out,
const sem::Call* call,
const sem::BuiltinFn* builtin) {
using Usage = core::ParameterUsage;
auto& signature = builtin->Signature();
auto* expr = call->Declaration();
auto arguments = expr->args;
// 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;
};
auto* texture = arg(Usage::kTexture);
if (TINT_UNLIKELY(!texture)) {
TINT_ICE() << "missing texture argument";
}
auto* texture_type = TypeOf(texture)->UnwrapRef()->As<core::type::Texture>();
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kTextureDimensions:
case wgsl::BuiltinFn::kTextureNumLayers:
case wgsl::BuiltinFn::kTextureNumLevels:
case wgsl::BuiltinFn::kTextureNumSamples: {
// All of these builtins use the GetDimensions() method on the texture
bool is_ms = texture_type->IsAnyOf<core::type::MultisampledTexture,
core::type::DepthMultisampledTexture>();
int num_dimensions = 0;
std::string swizzle;
switch (builtin->Fn()) {
case wgsl::BuiltinFn::kTextureDimensions:
switch (texture_type->dim()) {
case core::type::TextureDimension::kNone:
TINT_ICE() << "texture dimension is kNone";
case core::type::TextureDimension::k1d: