spirv-reader: flatten input IO arrays and matrices
Bug: tint:912 Change-Id: I403d504b577bda7918a81d990da5a13ca2036971 Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/56141 Auto-Submit: David Neto <dneto@google.com> Kokoro: Kokoro <noreply+kokoro@google.com> Reviewed-by: Ben Clayton <bclayton@google.com> Reviewed-by: James Price <jrprice@google.com>
This commit is contained in:
parent
5ea0fe00bf
commit
1d2f08b4ad
|
@ -935,6 +935,101 @@ ast::BlockStatement* FunctionEmitter::MakeFunctionBody() {
|
|||
return body;
|
||||
}
|
||||
|
||||
bool FunctionEmitter::EmitInputParameter(std::string var_name,
|
||||
const Type* var_type,
|
||||
ast::DecorationList* decos,
|
||||
std::vector<int> index_prefix,
|
||||
const Type* tip_type,
|
||||
const Type* forced_param_type,
|
||||
ast::VariableList* params,
|
||||
ast::StatementList* statements) {
|
||||
tip_type = tip_type->UnwrapAlias();
|
||||
if (auto* ref_type = tip_type->As<Reference>()) {
|
||||
tip_type = ref_type->type;
|
||||
}
|
||||
|
||||
if (auto* matrix_type = tip_type->As<Matrix>()) {
|
||||
index_prefix.push_back(0);
|
||||
const auto num_columns = static_cast<int>(matrix_type->columns);
|
||||
const Type* vec_ty = ty_.Vector(matrix_type->type, matrix_type->rows);
|
||||
for (int col = 0; col < num_columns; col++) {
|
||||
index_prefix.back() = col;
|
||||
if (!EmitInputParameter(var_name, var_type, decos, index_prefix, vec_ty,
|
||||
forced_param_type, params, statements)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return success();
|
||||
} else if (auto* array_type = tip_type->As<Array>()) {
|
||||
if (array_type->size == 0) {
|
||||
return Fail() << "runtime-size array not allowed on pipeline IO";
|
||||
}
|
||||
index_prefix.push_back(0);
|
||||
const Type* elem_ty = array_type->type;
|
||||
for (int i = 0; i < static_cast<int>(array_type->size); i++) {
|
||||
index_prefix.back() = i;
|
||||
if (!EmitInputParameter(var_name, var_type, decos, index_prefix, elem_ty,
|
||||
forced_param_type, params, statements)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return success();
|
||||
}
|
||||
|
||||
const bool is_builtin = ast::HasDecoration<ast::BuiltinDecoration>(*decos);
|
||||
|
||||
const Type* param_type = is_builtin ? forced_param_type : tip_type;
|
||||
|
||||
const auto param_name = namer_.MakeDerivedName(var_name + "_param");
|
||||
// Create the parameter.
|
||||
// TODO(dneto): Note: If the parameter has non-location decorations,
|
||||
// then those decoration AST nodes will be reused between multiple elements
|
||||
// of a matrix, array, or structure. Normally that's disallowed but currently
|
||||
// the SPIR-V reader will make duplicates when the entire AST is cloned
|
||||
// at the top level of the SPIR-V reader flow. Consider rewriting this
|
||||
// to avoid this node-sharing.
|
||||
params->push_back(
|
||||
builder_.Param(param_name, param_type->Build(builder_), *decos));
|
||||
|
||||
// Add a body statement to copy the parameter to the corresponding private
|
||||
// variable.
|
||||
ast::Expression* param_value = builder_.Expr(param_name);
|
||||
ast::Expression* store_dest = builder_.Expr(var_name);
|
||||
|
||||
// Index into the LHS as needed.
|
||||
auto* current_type = var_type->UnwrapAlias()->UnwrapRef()->UnwrapAlias();
|
||||
for (auto index : index_prefix) {
|
||||
if (auto* matrix_type = current_type->As<Matrix>()) {
|
||||
store_dest = builder_.IndexAccessor(store_dest, builder_.Expr(index));
|
||||
current_type = ty_.Vector(matrix_type->type, matrix_type->rows);
|
||||
} else if (auto* array_type = current_type->As<Array>()) {
|
||||
store_dest = builder_.IndexAccessor(store_dest, builder_.Expr(index));
|
||||
current_type = array_type->type->UnwrapAlias();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_builtin && (tip_type != forced_param_type)) {
|
||||
// The parameter will have the WGSL type, but we need bitcast to
|
||||
// the variable store type.
|
||||
param_value =
|
||||
create<ast::BitcastExpression>(tip_type->Build(builder_), param_value);
|
||||
}
|
||||
|
||||
statements->push_back(builder_.Assign(store_dest, param_value));
|
||||
|
||||
// Increment the location attribute, in case more parameters will follow.
|
||||
for (auto*& deco : *decos) {
|
||||
if (auto* loc_deco = deco->As<ast::LocationDecoration>()) {
|
||||
// Replace this location decoration with a new one with one higher index.
|
||||
// The old one doesn't leak because it's kept in the builder's AST node
|
||||
// list.
|
||||
deco = builder_.Location(loc_deco->source(), loc_deco->value() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return success();
|
||||
}
|
||||
|
||||
bool FunctionEmitter::EmitEntryPointAsWrapper() {
|
||||
Source source;
|
||||
|
||||
|
@ -954,9 +1049,9 @@ bool FunctionEmitter::EmitEntryPointAsWrapper() {
|
|||
TINT_ASSERT(Reader, var != nullptr);
|
||||
TINT_ASSERT(Reader, var->opcode() == SpvOpVariable);
|
||||
auto* store_type = GetVariableStoreType(*var);
|
||||
auto* forced_store_type = store_type;
|
||||
auto* forced_param_type = store_type;
|
||||
ast::DecorationList param_decos;
|
||||
if (!parser_impl_.ConvertDecorationsForVariable(var_id, &forced_store_type,
|
||||
if (!parser_impl_.ConvertDecorationsForVariable(var_id, &forced_param_type,
|
||||
¶m_decos, true)) {
|
||||
// This occurs, and is not an error, for the PointSize builtin.
|
||||
if (!success()) {
|
||||
|
@ -966,49 +1061,31 @@ bool FunctionEmitter::EmitEntryPointAsWrapper() {
|
|||
continue;
|
||||
}
|
||||
|
||||
// In Vulkan SPIR-V, Input variables must not have an initializer.
|
||||
// We don't have to handle initializers because in Vulkan SPIR-V, Input
|
||||
// variables must not have them.
|
||||
|
||||
const auto var_name = namer_.GetName(var_id);
|
||||
const auto var_sym = builder_.Symbols().Register(var_name);
|
||||
const auto param_name = namer_.MakeDerivedName(var_name + "_param");
|
||||
const auto param_sym = builder_.Symbols().Register(param_name);
|
||||
auto* param = create<ast::Variable>(
|
||||
source, param_sym, ast::StorageClass::kNone, ast::Access::kUndefined,
|
||||
forced_store_type->Build(builder_), true /* is const */,
|
||||
nullptr /* no constructor */, param_decos);
|
||||
decl.params.push_back(param);
|
||||
|
||||
// Add a body statement to copy the parameter to the corresponding private
|
||||
// variable.
|
||||
ast::Expression* param_value =
|
||||
create<ast::IdentifierExpression>(source, param_sym);
|
||||
ast::Expression* store_dest =
|
||||
create<ast::IdentifierExpression>(source, var_sym);
|
||||
bool ok = true;
|
||||
if (HasBuiltinSampleMask(param_decos)) {
|
||||
// In Vulkan SPIR-V, the sample mask is an array. In WGSL it's a scalar.
|
||||
// Use the first element only.
|
||||
store_dest = create<ast::ArrayAccessorExpression>(
|
||||
source, store_dest, parser_impl_.MakeNullValue(ty_.I32()));
|
||||
if (const auto* arr_ty = store_type->UnwrapAlias()->As<Array>()) {
|
||||
if (arr_ty->type->IsSignedScalarOrVector()) {
|
||||
// sample_mask is unsigned in WGSL. Bitcast it.
|
||||
param_value = create<ast::BitcastExpression>(
|
||||
source, ty_.I32()->Build(builder_), param_value);
|
||||
}
|
||||
} else {
|
||||
// Vulkan SPIR-V requires this. Validation should have failed already.
|
||||
return Fail()
|
||||
<< "expected SampleMask to be an array of integer scalars";
|
||||
}
|
||||
} else if (forced_store_type != store_type) {
|
||||
// The parameter will have the WGSL type, but we need to add
|
||||
// a bitcast to the variable store type.
|
||||
param_value = create<ast::BitcastExpression>(
|
||||
source, store_type->Build(builder_), param_value);
|
||||
auto* sample_mask_array_type =
|
||||
store_type->UnwrapRef()->UnwrapAlias()->As<Array>();
|
||||
TINT_ASSERT(Reader, sample_mask_array_type);
|
||||
ok = EmitInputParameter(var_name, store_type, ¶m_decos, {0},
|
||||
sample_mask_array_type->type, forced_param_type,
|
||||
&(decl.params), &stmts);
|
||||
} else {
|
||||
// The normal path.
|
||||
ok =
|
||||
EmitInputParameter(var_name, store_type, ¶m_decos, {}, store_type,
|
||||
forced_param_type, &(decl.params), &stmts);
|
||||
}
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stmts.push_back(
|
||||
create<ast::AssignmentStatement>(source, store_dest, param_value));
|
||||
}
|
||||
|
||||
// Call the inner function. It has no parameters.
|
||||
|
|
|
@ -410,6 +410,33 @@ class FunctionEmitter {
|
|||
/// @returns false if emission failed.
|
||||
bool EmitEntryPointAsWrapper();
|
||||
|
||||
/// Creates one or more entry point input parameters corresponding to a
|
||||
/// part of an input variable. The part of the input variable is specfied
|
||||
/// by the `index_prefix`, which successively indexes into the variable.
|
||||
/// Also generates the assignment statements that copy the input parameter
|
||||
/// to the corresponding part of the variable. Assumes the variable
|
||||
/// has already been created in the Private storage class.
|
||||
/// @param var_name The name of the variable
|
||||
/// @param var_type The store type of the variable
|
||||
/// @param decos The variable's decorations
|
||||
/// @param index_prefix Indices stepping into the variable, indicating
|
||||
/// what part of the variable to populate.
|
||||
/// @param tip_type The type of the component inside variable, after indexing
|
||||
/// with the indices in `index_prefix`.
|
||||
/// @param forced_param_type The type forced by WGSL, if the variable is a
|
||||
/// builtin, otherwise the same as var_type.
|
||||
/// @param params The parameter list where the new parameter is appended.
|
||||
/// @param statements The statement list where the assignment is appended.
|
||||
/// @returns false if emission failed
|
||||
bool EmitInputParameter(std::string var_name,
|
||||
const Type* var_type,
|
||||
ast::DecorationList* decos,
|
||||
std::vector<int> index_prefix,
|
||||
const Type* tip_type,
|
||||
const Type* forced_param_type,
|
||||
ast::VariableList* params,
|
||||
ast::StatementList* statements);
|
||||
|
||||
/// Create an ast::BlockStatement representing the body of the function.
|
||||
/// This creates the statement stack, which is non-empty for the lifetime
|
||||
/// of the function.
|
||||
|
|
|
@ -6288,7 +6288,404 @@ TEST_F(SpvModuleScopeVarParserTest,
|
|||
EXPECT_EQ(got, expected) << got;
|
||||
}
|
||||
|
||||
// TODO(dneto): pipeline IO: flatten structures, and distribute locations
|
||||
TEST_F(SpvModuleScopeVarParserTest, Input_FlattenArray_OneLevel) {
|
||||
const std::string assembly = R"(
|
||||
OpCapability Shader
|
||||
OpMemoryModel Logical Simple
|
||||
OpEntryPoint Vertex %main "main" %1 %2
|
||||
OpDecorate %1 Location 4
|
||||
OpDecorate %2 BuiltIn Position
|
||||
|
||||
%void = OpTypeVoid
|
||||
%voidfn = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%uint = OpTypeInt 32 0
|
||||
%uint_0 = OpConstant %uint 0
|
||||
%uint_1 = OpConstant %uint 1
|
||||
%uint_3 = OpConstant %uint 3
|
||||
%arr = OpTypeArray %float %uint_3
|
||||
%11 = OpTypePointer Input %arr
|
||||
|
||||
%1 = OpVariable %11 Input
|
||||
|
||||
%12 = OpTypePointer Output %v4float
|
||||
%2 = OpVariable %12 Output
|
||||
|
||||
%main = OpFunction %void None %voidfn
|
||||
%entry = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
auto p = parser(test::Assemble(assembly));
|
||||
|
||||
ASSERT_TRUE(p->Parse()) << p->error() << assembly;
|
||||
EXPECT_TRUE(p->error().empty());
|
||||
|
||||
const auto got = p->program().to_str();
|
||||
const std::string expected = R"(Module{
|
||||
Struct main_out {
|
||||
StructMember{[[ BuiltinDecoration{position}
|
||||
]] x_2: __vec_4__f32}
|
||||
}
|
||||
Variable{
|
||||
x_1
|
||||
private
|
||||
undefined
|
||||
__array__f32_3
|
||||
}
|
||||
Variable{
|
||||
x_2
|
||||
private
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
Function main_1 -> __void
|
||||
()
|
||||
{
|
||||
Return{}
|
||||
}
|
||||
Function main -> __type_name_main_out
|
||||
StageDecoration{vertex}
|
||||
(
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{4}
|
||||
}
|
||||
x_1_param
|
||||
none
|
||||
undefined
|
||||
__f32
|
||||
}
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{5}
|
||||
}
|
||||
x_1_param_1
|
||||
none
|
||||
undefined
|
||||
__f32
|
||||
}
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{6}
|
||||
}
|
||||
x_1_param_2
|
||||
none
|
||||
undefined
|
||||
__f32
|
||||
}
|
||||
)
|
||||
{
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{0}
|
||||
}
|
||||
Identifier[not set]{x_1_param}
|
||||
}
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{1}
|
||||
}
|
||||
Identifier[not set]{x_1_param_1}
|
||||
}
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{2}
|
||||
}
|
||||
Identifier[not set]{x_1_param_2}
|
||||
}
|
||||
Call[not set]{
|
||||
Identifier[not set]{main_1}
|
||||
(
|
||||
)
|
||||
}
|
||||
Return{
|
||||
{
|
||||
TypeConstructor[not set]{
|
||||
__type_name_main_out
|
||||
Identifier[not set]{x_2}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(got, expected) << got;
|
||||
}
|
||||
|
||||
TEST_F(SpvModuleScopeVarParserTest, Input_FlattenMatrix) {
|
||||
const std::string assembly = R"(
|
||||
OpCapability Shader
|
||||
OpMemoryModel Logical Simple
|
||||
OpEntryPoint Vertex %main "main" %1 %2
|
||||
OpDecorate %1 Location 9
|
||||
OpDecorate %2 BuiltIn Position
|
||||
|
||||
%void = OpTypeVoid
|
||||
%voidfn = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%m2v4float = OpTypeMatrix %v4float 2
|
||||
%uint = OpTypeInt 32 0
|
||||
|
||||
%11 = OpTypePointer Input %m2v4float
|
||||
|
||||
%1 = OpVariable %11 Input
|
||||
|
||||
%12 = OpTypePointer Output %v4float
|
||||
%2 = OpVariable %12 Output
|
||||
|
||||
%main = OpFunction %void None %voidfn
|
||||
%entry = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
auto p = parser(test::Assemble(assembly));
|
||||
|
||||
ASSERT_TRUE(p->Parse()) << p->error() << assembly;
|
||||
EXPECT_TRUE(p->error().empty());
|
||||
|
||||
const auto got = p->program().to_str();
|
||||
const std::string expected = R"(Module{
|
||||
Struct main_out {
|
||||
StructMember{[[ BuiltinDecoration{position}
|
||||
]] x_2: __vec_4__f32}
|
||||
}
|
||||
Variable{
|
||||
x_1
|
||||
private
|
||||
undefined
|
||||
__mat_4_2__f32
|
||||
}
|
||||
Variable{
|
||||
x_2
|
||||
private
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
Function main_1 -> __void
|
||||
()
|
||||
{
|
||||
Return{}
|
||||
}
|
||||
Function main -> __type_name_main_out
|
||||
StageDecoration{vertex}
|
||||
(
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{9}
|
||||
}
|
||||
x_1_param
|
||||
none
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{10}
|
||||
}
|
||||
x_1_param_1
|
||||
none
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
)
|
||||
{
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{0}
|
||||
}
|
||||
Identifier[not set]{x_1_param}
|
||||
}
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{1}
|
||||
}
|
||||
Identifier[not set]{x_1_param_1}
|
||||
}
|
||||
Call[not set]{
|
||||
Identifier[not set]{main_1}
|
||||
(
|
||||
)
|
||||
}
|
||||
Return{
|
||||
{
|
||||
TypeConstructor[not set]{
|
||||
__type_name_main_out
|
||||
Identifier[not set]{x_2}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(got, expected) << got;
|
||||
}
|
||||
|
||||
TEST_F(SpvModuleScopeVarParserTest, Input_FlattenNested) {
|
||||
const std::string assembly = R"(
|
||||
OpCapability Shader
|
||||
OpMemoryModel Logical Simple
|
||||
OpEntryPoint Vertex %main "main" %1 %2
|
||||
OpDecorate %1 Location 7
|
||||
OpDecorate %2 BuiltIn Position
|
||||
|
||||
%void = OpTypeVoid
|
||||
%voidfn = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%m2v4float = OpTypeMatrix %v4float 2
|
||||
%uint = OpTypeInt 32 0
|
||||
%uint_2 = OpConstant %uint 2
|
||||
|
||||
%arr = OpTypeArray %m2v4float %uint_2
|
||||
|
||||
%11 = OpTypePointer Input %arr
|
||||
%1 = OpVariable %11 Input
|
||||
|
||||
%12 = OpTypePointer Output %v4float
|
||||
%2 = OpVariable %12 Output
|
||||
|
||||
%main = OpFunction %void None %voidfn
|
||||
%entry = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
auto p = parser(test::Assemble(assembly));
|
||||
|
||||
ASSERT_TRUE(p->Parse()) << p->error() << assembly;
|
||||
EXPECT_TRUE(p->error().empty());
|
||||
|
||||
const auto got = p->program().to_str();
|
||||
const std::string expected = R"(Module{
|
||||
Struct main_out {
|
||||
StructMember{[[ BuiltinDecoration{position}
|
||||
]] x_2: __vec_4__f32}
|
||||
}
|
||||
Variable{
|
||||
x_1
|
||||
private
|
||||
undefined
|
||||
__array__mat_4_2__f32_2
|
||||
}
|
||||
Variable{
|
||||
x_2
|
||||
private
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
Function main_1 -> __void
|
||||
()
|
||||
{
|
||||
Return{}
|
||||
}
|
||||
Function main -> __type_name_main_out
|
||||
StageDecoration{vertex}
|
||||
(
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{7}
|
||||
}
|
||||
x_1_param
|
||||
none
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{8}
|
||||
}
|
||||
x_1_param_1
|
||||
none
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{9}
|
||||
}
|
||||
x_1_param_2
|
||||
none
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
VariableConst{
|
||||
Decorations{
|
||||
LocationDecoration{10}
|
||||
}
|
||||
x_1_param_3
|
||||
none
|
||||
undefined
|
||||
__vec_4__f32
|
||||
}
|
||||
)
|
||||
{
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{0}
|
||||
}
|
||||
ScalarConstructor[not set]{0}
|
||||
}
|
||||
Identifier[not set]{x_1_param}
|
||||
}
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{0}
|
||||
}
|
||||
ScalarConstructor[not set]{1}
|
||||
}
|
||||
Identifier[not set]{x_1_param_1}
|
||||
}
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{1}
|
||||
}
|
||||
ScalarConstructor[not set]{0}
|
||||
}
|
||||
Identifier[not set]{x_1_param_2}
|
||||
}
|
||||
Assignment{
|
||||
ArrayAccessor[not set]{
|
||||
ArrayAccessor[not set]{
|
||||
Identifier[not set]{x_1}
|
||||
ScalarConstructor[not set]{1}
|
||||
}
|
||||
ScalarConstructor[not set]{1}
|
||||
}
|
||||
Identifier[not set]{x_1_param_3}
|
||||
}
|
||||
Call[not set]{
|
||||
Identifier[not set]{main_1}
|
||||
(
|
||||
)
|
||||
}
|
||||
Return{
|
||||
{
|
||||
TypeConstructor[not set]{
|
||||
__type_name_main_out
|
||||
Identifier[not set]{x_2}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(got, expected) << got;
|
||||
}
|
||||
|
||||
// TODO(dneto): flatting structures
|
||||
|
||||
} // namespace
|
||||
} // namespace spirv
|
||||
|
|
Loading…
Reference in New Issue