tint: Add support for atomic ops to spirv reader

The following operations are supported:

OpAtomicLoad
OpAtomicStore
OpAtomicExchange
OpAtomicCompareExchange
OpAtomicCompareExchangeWeak
OpAtomicIIncrement
OpAtomicIDecrement
OpAtomicIAdd
OpAtomicISub
OpAtomicSMin
OpAtomicUMin
OpAtomicSMax
OpAtomicUMax
OpAtomicAnd
OpAtomicOr
OpAtomicXor

These are not, but may be supported in the future:

OpAtomicFlagTestAndSet
OpAtomicFlagClear
OpAtomicFMinEXT
OpAtomicFMaxEXT
OpAtomicFAddEXT

Bug: tint:1441
Change-Id: Ifd53643b38d43664905a0dddfca609add4914670
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/94121
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
This commit is contained in:
Antonio Maiorano
2022-06-24 22:28:23 +00:00
committed by Dawn LUCI CQ
parent 57810d81d5
commit 268d7b8357
696 changed files with 36681 additions and 6 deletions

View File

@@ -526,6 +526,8 @@ libtint_source_set("libtint_core_all_src") {
"transform/simplify_pointers.h",
"transform/single_entry_point.cc",
"transform/single_entry_point.h",
"transform/spirv_atomic.cc",
"transform/spirv_atomic.h",
"transform/transform.cc",
"transform/transform.h",
"transform/unshadow.cc",
@@ -1190,6 +1192,7 @@ if (tint_build_unittests) {
"transform/robustness_test.cc",
"transform/simplify_pointers_test.cc",
"transform/single_entry_point_test.cc",
"transform/spirv_atomic_test.cc",
"transform/test_helper.h",
"transform/transform_test.cc",
"transform/unshadow_test.cc",

View File

@@ -450,6 +450,8 @@ set(TINT_LIB_SRCS
transform/simplify_pointers.h
transform/single_entry_point.cc
transform/single_entry_point.h
transform/spirv_atomic.cc
transform/spirv_atomic.h
transform/transform.cc
transform/transform.h
transform/unshadow.cc
@@ -1114,6 +1116,7 @@ if(TINT_BUILD_TESTS)
transform/robustness_test.cc
transform/simplify_pointers_test.cc
transform/single_entry_point_test.cc
transform/spirv_atomic_test.cc
transform/test_helper.h
transform/unshadow_test.cc
transform/unwind_discard_functions_test.cc

View File

@@ -36,6 +36,7 @@
#include "src/tint/sem/builtin_type.h"
#include "src/tint/sem/depth_texture.h"
#include "src/tint/sem/sampled_texture.h"
#include "src/tint/transform/spirv_atomic.h"
// Terms:
// CFG: the control flow graph of the function, where basic blocks are the
@@ -500,6 +501,38 @@ bool IsSampledImageAccess(SpvOp opcode) {
return false;
}
// @param opcode a SPIR-V opcode
// @returns true if the given instruction is an atomic operation.
bool IsAtomicOp(SpvOp opcode) {
switch (opcode) {
case SpvOpAtomicLoad:
case SpvOpAtomicStore:
case SpvOpAtomicExchange:
case SpvOpAtomicCompareExchange:
case SpvOpAtomicCompareExchangeWeak:
case SpvOpAtomicIIncrement:
case SpvOpAtomicIDecrement:
case SpvOpAtomicIAdd:
case SpvOpAtomicISub:
case SpvOpAtomicSMin:
case SpvOpAtomicUMin:
case SpvOpAtomicSMax:
case SpvOpAtomicUMax:
case SpvOpAtomicAnd:
case SpvOpAtomicOr:
case SpvOpAtomicXor:
case SpvOpAtomicFlagTestAndSet:
case SpvOpAtomicFlagClear:
case SpvOpAtomicFMinEXT:
case SpvOpAtomicFMaxEXT:
case SpvOpAtomicFAddEXT:
return true;
default:
break;
}
return false;
}
// @param opcode a SPIR-V opcode
// @returns true if the given instruction is an image sampling, gather,
// or gather-compare operation.
@@ -3487,6 +3520,10 @@ bool FunctionEmitter::EmitStatement(const spvtools::opt::Instruction& inst) {
return EmitImageAccess(inst);
}
if (IsAtomicOp(inst.opcode())) {
return EmitAtomicOp(inst);
}
switch (inst.opcode()) {
case SpvOpNop:
return true;
@@ -5417,6 +5454,115 @@ bool FunctionEmitter::EmitImageQuery(const spvtools::opt::Instruction& inst) {
return Fail() << "unhandled image query: " << inst.PrettyPrint();
}
bool FunctionEmitter::EmitAtomicOp(const spvtools::opt::Instruction& inst) {
auto emit_atomic = [&](sem::BuiltinType builtin, std::initializer_list<TypedExpression> args) {
// Split args into params and expressions
ast::ParameterList params;
params.reserve(args.size());
ast::ExpressionList exprs;
exprs.reserve(args.size());
size_t i = 0;
for (auto& a : args) {
params.emplace_back(builder_.Param("p" + std::to_string(i++), a.type->Build(builder_)));
exprs.emplace_back(a.expr);
}
// Function return type
const ast::Type* ret_type = nullptr;
if (inst.type_id() != 0) {
ret_type = parser_impl_.ConvertType(inst.type_id())->Build(builder_);
} else {
ret_type = builder_.ty.void_();
}
// Emit stub, will be removed by transform::SpirvAtomic
auto sym = builder_.Symbols().New(std::string("stub_") + sem::str(builtin));
auto* stub_deco =
builder_.ASTNodes().Create<transform::SpirvAtomic::Stub>(builder_.ID(), builtin);
auto* stub =
create<ast::Function>(Source{}, sym, std::move(params), ret_type,
/* body */ nullptr,
ast::AttributeList{
stub_deco,
builder_.Disable(ast::DisabledValidation::kFunctionHasNoBody),
},
ast::AttributeList{});
builder_.AST().AddFunction(stub);
// Emit call to stub, will be replaced with call to atomic builtin by transform::SpirvAtomic
auto* call = builder_.Call(Source{}, sym, exprs);
if (inst.type_id() != 0) {
auto* result_type = parser_impl_.ConvertType(inst.type_id());
TypedExpression expr{result_type, call};
return EmitConstDefOrWriteToHoistedVar(inst, expr);
}
AddStatement(create<ast::CallStatement>(call));
return true;
};
auto oper = [&](uint32_t index) -> TypedExpression { //
return MakeOperand(inst, index);
};
auto lit = [&](int v) -> TypedExpression {
auto* result_type = parser_impl_.ConvertType(inst.type_id());
if (result_type->Is<I32>()) {
return TypedExpression(result_type, builder_.Expr(i32(v)));
} else if (result_type->Is<U32>()) {
return TypedExpression(result_type, builder_.Expr(u32(v)));
}
return {};
};
switch (inst.opcode()) {
case SpvOpAtomicLoad:
return emit_atomic(sem::BuiltinType::kAtomicLoad, {oper(/*ptr*/ 0)});
case SpvOpAtomicStore:
return emit_atomic(sem::BuiltinType::kAtomicStore,
{oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicExchange:
return emit_atomic(sem::BuiltinType::kAtomicExchange,
{oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicCompareExchange:
case SpvOpAtomicCompareExchangeWeak:
return emit_atomic(sem::BuiltinType::kAtomicCompareExchangeWeak,
{oper(/*ptr*/ 0), /*value*/ oper(5), /*comparator*/ oper(4)});
case SpvOpAtomicIIncrement:
return emit_atomic(sem::BuiltinType::kAtomicAdd, {oper(/*ptr*/ 0), lit(1)});
case SpvOpAtomicIDecrement:
return emit_atomic(sem::BuiltinType::kAtomicSub, {oper(/*ptr*/ 0), lit(1)});
case SpvOpAtomicIAdd:
return emit_atomic(sem::BuiltinType::kAtomicAdd, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicISub:
return emit_atomic(sem::BuiltinType::kAtomicSub, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicSMin:
return emit_atomic(sem::BuiltinType::kAtomicMin, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicUMin:
return emit_atomic(sem::BuiltinType::kAtomicMin, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicSMax:
return emit_atomic(sem::BuiltinType::kAtomicMax, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicUMax:
return emit_atomic(sem::BuiltinType::kAtomicMax, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicAnd:
return emit_atomic(sem::BuiltinType::kAtomicAnd, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicOr:
return emit_atomic(sem::BuiltinType::kAtomicOr, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicXor:
return emit_atomic(sem::BuiltinType::kAtomicXor, {oper(/*ptr*/ 0), oper(/*value*/ 3)});
case SpvOpAtomicFlagTestAndSet:
case SpvOpAtomicFlagClear:
case SpvOpAtomicFMinEXT:
case SpvOpAtomicFMaxEXT:
case SpvOpAtomicFAddEXT:
return Fail() << "unsupported atomic op: " << inst.PrettyPrint();
default:
break;
}
return Fail() << "unhandled atomic op: " << inst.PrettyPrint();
}
ast::ExpressionList FunctionEmitter::MakeCoordinateOperandsForImageAccess(
const spvtools::opt::Instruction& inst) {
if (!parser_impl_.success()) {

View File

@@ -1060,7 +1060,7 @@ class FunctionEmitter {
/// Emits a texture builtin function call for a SPIR-V instruction that
/// accesses an image or sampled image.
/// @param inst the SPIR-V instruction
/// @returns an expression
/// @returns true on success, false on error
bool EmitImageAccess(const spvtools::opt::Instruction& inst);
/// Emits statements to implement a SPIR-V image query.
@@ -1068,6 +1068,11 @@ class FunctionEmitter {
/// @returns an expression
bool EmitImageQuery(const spvtools::opt::Instruction& inst);
/// Emits statements to implement a SPIR-V atomic op.
/// @param inst the SPIR-V instruction
/// @returns true on success, false on error
bool EmitAtomicOp(const spvtools::opt::Instruction& inst);
/// Converts the given texel to match the type required for the storage
/// texture with the given type. In WGSL the texel value is always provided
/// as a 4-element vector, but the component type is determined by the

View File

@@ -22,6 +22,7 @@
#include "src/tint/transform/manager.h"
#include "src/tint/transform/remove_unreachable_statements.h"
#include "src/tint/transform/simplify_pointers.h"
#include "src/tint/transform/spirv_atomic.h"
#include "src/tint/transform/unshadow.h"
namespace tint::reader::spirv {
@@ -55,6 +56,7 @@ Program Parse(const std::vector<uint32_t>& input) {
manager.Add<transform::DecomposeStridedMatrix>();
manager.Add<transform::DecomposeStridedArray>();
manager.Add<transform::RemoveUnreachableStatements>();
manager.Add<transform::SpirvAtomic>();
return manager.Run(&program).program;
}

View File

@@ -101,11 +101,7 @@
<DisplayString>{*variable};</DisplayString>
</Type>
<Type Name="tint::ast::UintLiteralExpression">
<DisplayString>{value}</DisplayString>
</Type>
<Type Name="tint::ast::SintLiteralExpression">
<Type Name="tint::ast::IntLiteralExpression">
<DisplayString>{value}</DisplayString>
</Type>

View File

@@ -0,0 +1,231 @@
// Copyright 2022 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "src/tint/transform/spirv_atomic.h"
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "src/tint/program_builder.h"
#include "src/tint/sem/block_statement.h"
#include "src/tint/sem/function.h"
#include "src/tint/sem/index_accessor_expression.h"
#include "src/tint/sem/member_accessor_expression.h"
#include "src/tint/sem/reference.h"
#include "src/tint/sem/statement.h"
#include "src/tint/utils/map.h"
#include "src/tint/utils/unique_vector.h"
TINT_INSTANTIATE_TYPEINFO(tint::transform::SpirvAtomic);
TINT_INSTANTIATE_TYPEINFO(tint::transform::SpirvAtomic::Stub);
namespace tint::transform {
/// Private implementation of transform
struct SpirvAtomic::State {
private:
/// A struct that has been forked because a subset of members were made atomic.
struct ForkedStruct {
Symbol name;
std::unordered_set<size_t> atomic_members;
};
CloneContext& ctx;
ProgramBuilder& b = *ctx.dst;
std::unordered_map<const ast::Struct*, ForkedStruct> forked_structs;
std::unordered_set<const sem::Variable*> atomic_variables;
utils::UniqueVector<const sem::Expression*> atomic_expressions;
public:
/// Constructor
/// @param c the clone context
explicit State(CloneContext& c) : ctx(c) {}
/// Runs the transform
void Run() {
// Look for stub functions generated by the SPIR-V reader, which are used as placeholders
// for atomic builtin calls.
for (auto* fn : ctx.src->AST().Functions()) {
if (auto* stub = ast::GetAttribute<Stub>(fn->attributes)) {
auto* sem = ctx.src->Sem().Get(fn);
for (auto* call : sem->CallSites()) {
// The first argument is always the atomic.
// The stub passes this by value, whereas the builtin wants a pointer.
// Take the address of the atomic argument.
auto& args = call->Declaration()->args;
auto out_args = ctx.Clone(args);
out_args[0] = b.AddressOf(out_args[0]);
// Replace all callsites of this stub to a call to the real builtin
if (stub->builtin == sem::BuiltinType::kAtomicCompareExchangeWeak) {
// atomicCompareExchangeWeak returns a struct, so insert a call to it above
// the current statement, and replace the current call with the struct's
// `old_value` member.
auto* block = call->Stmt()->Block()->Declaration();
auto old_value = b.Symbols().New("old_value");
auto old_value_decl = b.Decl(b.Let(
old_value, nullptr,
b.MemberAccessor(b.Call(sem::str(stub->builtin), std::move(out_args)),
b.Expr("old_value"))));
ctx.InsertBefore(block->statements, call->Stmt()->Declaration(),
old_value_decl);
ctx.Replace(call->Declaration(), b.Expr(old_value));
} else {
ctx.Replace(call->Declaration(),
b.Call(sem::str(stub->builtin), std::move(out_args)));
}
// Keep track of this expression. We'll need to modify the source variable /
// structure to be atomic.
atomic_expressions.add(ctx.src->Sem().Get(args[0]));
}
// Remove the stub from the output program
ctx.Remove(ctx.src->AST().GlobalDeclarations(), fn);
}
}
// Transform all variables and structure members that were used in atomic operations as
// atomic types. This propagates up originating expression chains.
ProcessAtomicExpressions();
// If we need to change structure members, then fork them.
if (!forked_structs.empty()) {
ctx.ReplaceAll([&](const ast::Struct* str) {
// Always emit the original structure. This allows unrelated usage of the structure
// to continue working.
// auto* original = ctx.CloneWithoutTransform(str);
// Is `str` a structure we need to fork?
if (auto it = forked_structs.find(str); it != forked_structs.end()) {
const auto& forked = it->second;
// Re-create the structure swapping in the atomic-flavoured members
std::vector<const ast::StructMember*> members(str->members.size());
for (size_t i = 0; i < str->members.size(); i++) {
auto* member = str->members[i];
if (forked.atomic_members.count(i)) {
auto* type = AtomicTypeFor(ctx.src->Sem().Get(member)->Type());
auto name = ctx.src->Symbols().NameFor(member->symbol);
members[i] = b.Member(name, type, ctx.Clone(member->attributes));
} else {
members[i] = ctx.Clone(member);
}
}
b.Structure(forked.name, std::move(members));
}
// return original;
return nullptr;
});
}
ctx.Clone();
}
private:
ForkedStruct& Fork(const ast::Struct* str) {
auto& forked = forked_structs[str];
if (!forked.name.IsValid()) {
forked.name = b.Symbols().New(ctx.src->Symbols().NameFor(str->name) + "_atomic");
}
return forked;
}
void ProcessAtomicExpressions() {
for (size_t i = 0; i < atomic_expressions.size(); i++) {
Switch(
atomic_expressions[i], //
[&](const sem::VariableUser* user) {
auto* v = user->Variable()->Declaration();
if (v->type && atomic_variables.emplace(user->Variable()).second) {
ctx.Replace(v->type, AtomicTypeFor(user->Variable()->Type()));
}
if (auto* ctor = user->Variable()->Constructor()) {
atomic_expressions.add(ctor);
}
},
[&](const sem::StructMemberAccess* access) {
// Fork the struct (the first time) and mark member(s) that need to be made
// atomic.
auto* member = access->Member();
Fork(member->Struct()->Declaration()).atomic_members.emplace(member->Index());
atomic_expressions.add(access->Object());
},
[&](const sem::IndexAccessorExpression* index) {
atomic_expressions.add(index->Object());
},
[&](const sem::Expression* e) {
if (auto* unary = e->Declaration()->As<ast::UnaryOpExpression>()) {
atomic_expressions.add(ctx.src->Sem().Get(unary->expr));
}
});
}
}
const ast::Type* AtomicTypeFor(const sem::Type* ty) {
return Switch(
ty, //
[&](const sem::I32*) { return b.ty.atomic(CreateASTTypeFor(ctx, ty)); },
[&](const sem::U32*) { return b.ty.atomic(CreateASTTypeFor(ctx, ty)); },
[&](const sem::Struct* str) { return b.ty.type_name(Fork(str->Declaration()).name); },
[&](const sem::Array* arr) {
return arr->IsRuntimeSized()
? b.ty.array(AtomicTypeFor(arr->ElemType()))
: b.ty.array(AtomicTypeFor(arr->ElemType()), u32(arr->Count()));
},
[&](const sem::Pointer* ptr) {
return b.ty.pointer(AtomicTypeFor(ptr->StoreType()), ptr->StorageClass(),
ptr->Access());
},
[&](const sem::Reference* ref) { return AtomicTypeFor(ref->StoreType()); },
[&](Default) {
TINT_ICE(Transform, b.Diagnostics())
<< "unhandled type: " << ty->FriendlyName(ctx.src->Symbols());
return nullptr;
});
}
};
SpirvAtomic::SpirvAtomic() = default;
SpirvAtomic::~SpirvAtomic() = default;
SpirvAtomic::Stub::Stub(ProgramID pid, sem::BuiltinType b) : Base(pid), builtin(b) {}
SpirvAtomic::Stub::~Stub() = default;
std::string SpirvAtomic::Stub::InternalName() const {
return "@internal(spirv-atomic " + std::string(sem::str(builtin)) + ")";
}
const SpirvAtomic::Stub* SpirvAtomic::Stub::Clone(CloneContext* ctx) const {
return ctx->dst->ASTNodes().Create<SpirvAtomic::Stub>(ctx->dst->ID(), builtin);
}
bool SpirvAtomic::ShouldRun(const Program* program, const DataMap&) const {
for (auto* fn : program->AST().Functions()) {
if (ast::HasAttribute<Stub>(fn->attributes)) {
return true;
}
}
return false;
}
void SpirvAtomic::Run(CloneContext& ctx, const DataMap&, DataMap&) const {
State{ctx}.Run();
}
} // namespace tint::transform

View File

@@ -0,0 +1,84 @@
// Copyright 2022 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef SRC_TINT_TRANSFORM_SPIRV_ATOMIC_H_
#define SRC_TINT_TRANSFORM_SPIRV_ATOMIC_H_
#include <string>
#include "src/tint/ast/internal_attribute.h"
#include "src/tint/sem/builtin_type.h"
#include "src/tint/transform/transform.h"
// Forward declarations
namespace tint {
class CloneContext;
} // namespace tint
namespace tint::transform {
/// SpirvAtomic is a transform that replaces calls to stub functions created by the SPIR-V reader
/// with calls to the WGSL atomic builtin. It also makes sure to replace variable declarations that
/// are the target of the atomic operations with an atomic declaration of the same type. For
/// structs, it creates a copy of the original struct with atomic members.
class SpirvAtomic final : public Castable<SpirvAtomic, Transform> {
public:
/// Constructor
SpirvAtomic();
/// Destructor
~SpirvAtomic() override;
/// Stub is an attribute applied to stub SPIR-V reader generated functions that need to be
/// translated to an atomic builtin.
class Stub final : public Castable<Stub, ast::InternalAttribute> {
public:
/// @param program_id the identifier of the program that owns this node
/// @param builtin the atomic builtin this stub represents
Stub(ProgramID program_id, sem::BuiltinType builtin);
/// Destructor
~Stub() override;
/// @return a short description of the internal attribute which will be
/// displayed as `@internal(<name>)`
std::string InternalName() const override;
/// Performs a deep clone of this object using the CloneContext `ctx`.
/// @param ctx the clone context
/// @return the newly cloned object
const Stub* Clone(CloneContext* ctx) const override;
/// The type of the intrinsic
const sem::BuiltinType builtin;
};
/// @param program the program to inspect
/// @param data optional extra transform-specific input data
/// @returns true if this transform should be run for the given program
bool ShouldRun(const Program* program, const DataMap& data = {}) const override;
protected:
struct State;
/// Runs the transform using the CloneContext built for transforming a
/// program. Run() is responsible for calling Clone() on the CloneContext.
/// @param ctx the CloneContext primed with the input program and
/// ProgramBuilder
/// @param inputs optional extra transform-specific input data
/// @param outputs optional extra transform-specific output data
void Run(CloneContext& ctx, const DataMap& inputs, DataMap& outputs) const override;
};
} // namespace tint::transform
#endif // SRC_TINT_TRANSFORM_SPIRV_ATOMIC_H_

File diff suppressed because it is too large Load Diff