SPIRV-Cross-Vulnerable/spirv_common.hpp
Jan Sikorski 44a5f1fc3e MSL: Add support for overlapping bindings.
This adds support for bindings which share the same DescriptorSet/Binding pair.

The motivating example is vkd3d, which uses overlapping arrays of resources to
emulate D3D12 descriptor tables. The generated MSL argument buffer only
includes the first resource (in this example 't0'):

struct spvDescriptorSetBuffer2
{
    array<texture2d<float>, 499968> t0 [[id(0)]];
    // Overlapping binding: array<texture3d<float>, 499968> t2 [[id(0)]];
};

When t2 is referenced, we cast the instantiated member:

float4 r1 = spvDescriptorSet2.t0[_79].sample(...);
float4 r2 = (*(constant array<texture3d<float>, 499968>*)&spvDescriptorSet2.t0)[_97].sample(...);
2024-04-02 12:01:07 +02:00

1944 lines
47 KiB
C++

/*
* Copyright 2015-2021 Arm Limited
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
* 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.
*/
/*
* At your option, you may choose to accept this material under either:
* 1. The Apache License, Version 2.0, found at <http://www.apache.org/licenses/LICENSE-2.0>, or
* 2. The MIT License, found at <http://opensource.org/licenses/MIT>.
*/
#ifndef SPIRV_CROSS_COMMON_HPP
#define SPIRV_CROSS_COMMON_HPP
#ifndef SPV_ENABLE_UTILITY_CODE
#define SPV_ENABLE_UTILITY_CODE
#endif
#include "spirv.hpp"
#include "spirv_cross_containers.hpp"
#include "spirv_cross_error_handling.hpp"
#include <functional>
// A bit crude, but allows projects which embed SPIRV-Cross statically to
// effectively hide all the symbols from other projects.
// There is a case where we have:
// - Project A links against SPIRV-Cross statically.
// - Project A links against Project B statically.
// - Project B links against SPIRV-Cross statically (might be a different version).
// This leads to a conflict with extremely bizarre results.
// By overriding the namespace in one of the project builds, we can work around this.
// If SPIRV-Cross is embedded in dynamic libraries,
// prefer using -fvisibility=hidden on GCC/Clang instead.
#ifdef SPIRV_CROSS_NAMESPACE_OVERRIDE
#define SPIRV_CROSS_NAMESPACE SPIRV_CROSS_NAMESPACE_OVERRIDE
#else
#define SPIRV_CROSS_NAMESPACE spirv_cross
#endif
namespace SPIRV_CROSS_NAMESPACE
{
namespace inner
{
template <typename T>
void join_helper(StringStream<> &stream, T &&t)
{
stream << std::forward<T>(t);
}
template <typename T, typename... Ts>
void join_helper(StringStream<> &stream, T &&t, Ts &&... ts)
{
stream << std::forward<T>(t);
join_helper(stream, std::forward<Ts>(ts)...);
}
} // namespace inner
class Bitset
{
public:
Bitset() = default;
explicit inline Bitset(uint64_t lower_)
: lower(lower_)
{
}
inline bool get(uint32_t bit) const
{
if (bit < 64)
return (lower & (1ull << bit)) != 0;
else
return higher.count(bit) != 0;
}
inline void set(uint32_t bit)
{
if (bit < 64)
lower |= 1ull << bit;
else
higher.insert(bit);
}
inline void clear(uint32_t bit)
{
if (bit < 64)
lower &= ~(1ull << bit);
else
higher.erase(bit);
}
inline uint64_t get_lower() const
{
return lower;
}
inline void reset()
{
lower = 0;
higher.clear();
}
inline void merge_and(const Bitset &other)
{
lower &= other.lower;
std::unordered_set<uint32_t> tmp_set;
for (auto &v : higher)
if (other.higher.count(v) != 0)
tmp_set.insert(v);
higher = std::move(tmp_set);
}
inline void merge_or(const Bitset &other)
{
lower |= other.lower;
for (auto &v : other.higher)
higher.insert(v);
}
inline bool operator==(const Bitset &other) const
{
if (lower != other.lower)
return false;
if (higher.size() != other.higher.size())
return false;
for (auto &v : higher)
if (other.higher.count(v) == 0)
return false;
return true;
}
inline bool operator!=(const Bitset &other) const
{
return !(*this == other);
}
template <typename Op>
void for_each_bit(const Op &op) const
{
// TODO: Add ctz-based iteration.
for (uint32_t i = 0; i < 64; i++)
{
if (lower & (1ull << i))
op(i);
}
if (higher.empty())
return;
// Need to enforce an order here for reproducible results,
// but hitting this path should happen extremely rarely, so having this slow path is fine.
SmallVector<uint32_t> bits;
bits.reserve(higher.size());
for (auto &v : higher)
bits.push_back(v);
std::sort(std::begin(bits), std::end(bits));
for (auto &v : bits)
op(v);
}
inline bool empty() const
{
return lower == 0 && higher.empty();
}
private:
// The most common bits to set are all lower than 64,
// so optimize for this case. Bits spilling outside 64 go into a slower data structure.
// In almost all cases, higher data structure will not be used.
uint64_t lower = 0;
std::unordered_set<uint32_t> higher;
};
// Helper template to avoid lots of nasty string temporary munging.
template <typename... Ts>
std::string join(Ts &&... ts)
{
StringStream<> stream;
inner::join_helper(stream, std::forward<Ts>(ts)...);
return stream.str();
}
inline std::string merge(const SmallVector<std::string> &list, const char *between = ", ")
{
StringStream<> stream;
for (auto &elem : list)
{
stream << elem;
if (&elem != &list.back())
stream << between;
}
return stream.str();
}
// Make sure we don't accidentally call this with float or doubles with SFINAE.
// Have to use the radix-aware overload.
template <typename T, typename std::enable_if<!std::is_floating_point<T>::value, int>::type = 0>
inline std::string convert_to_string(const T &t)
{
return std::to_string(t);
}
static inline std::string convert_to_string(int32_t value)
{
// INT_MIN is ... special on some backends. If we use a decimal literal, and negate it, we
// could accidentally promote the literal to long first, then negate.
// To workaround it, emit int(0x80000000) instead.
if (value == (std::numeric_limits<int32_t>::min)())
return "int(0x80000000)";
else
return std::to_string(value);
}
static inline std::string convert_to_string(int64_t value, const std::string &int64_type, bool long_long_literal_suffix)
{
// INT64_MIN is ... special on some backends.
// If we use a decimal literal, and negate it, we might overflow the representable numbers.
// To workaround it, emit int(0x80000000) instead.
if (value == (std::numeric_limits<int64_t>::min)())
return join(int64_type, "(0x8000000000000000u", (long_long_literal_suffix ? "ll" : "l"), ")");
else
return std::to_string(value) + (long_long_literal_suffix ? "ll" : "l");
}
// Allow implementations to set a convenient standard precision
#ifndef SPIRV_CROSS_FLT_FMT
#define SPIRV_CROSS_FLT_FMT "%.32g"
#endif
// Disable sprintf and strcat warnings.
// We cannot rely on snprintf and family existing because, ..., MSVC.
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#elif defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable : 4996)
#endif
static inline void fixup_radix_point(char *str, char radix_point)
{
// Setting locales is a very risky business in multi-threaded program,
// so just fixup locales instead. We only need to care about the radix point.
if (radix_point != '.')
{
while (*str != '\0')
{
if (*str == radix_point)
*str = '.';
str++;
}
}
}
inline std::string convert_to_string(float t, char locale_radix_point)
{
// std::to_string for floating point values is broken.
// Fallback to something more sane.
char buf[64];
sprintf(buf, SPIRV_CROSS_FLT_FMT, t);
fixup_radix_point(buf, locale_radix_point);
// Ensure that the literal is float.
if (!strchr(buf, '.') && !strchr(buf, 'e'))
strcat(buf, ".0");
return buf;
}
inline std::string convert_to_string(double t, char locale_radix_point)
{
// std::to_string for floating point values is broken.
// Fallback to something more sane.
char buf[64];
sprintf(buf, SPIRV_CROSS_FLT_FMT, t);
fixup_radix_point(buf, locale_radix_point);
// Ensure that the literal is float.
if (!strchr(buf, '.') && !strchr(buf, 'e'))
strcat(buf, ".0");
return buf;
}
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#elif defined(_MSC_VER)
#pragma warning(pop)
#endif
class FloatFormatter
{
public:
virtual ~FloatFormatter() = default;
virtual std::string format_float(float value) = 0;
virtual std::string format_double(double value) = 0;
};
template <typename T>
struct ValueSaver
{
explicit ValueSaver(T &current_)
: current(current_)
, saved(current_)
{
}
void release()
{
current = saved;
}
~ValueSaver()
{
release();
}
T &current;
T saved;
};
struct Instruction
{
uint16_t op = 0;
uint16_t count = 0;
// If offset is 0 (not a valid offset into the instruction stream),
// we have an instruction stream which is embedded in the object.
uint32_t offset = 0;
uint32_t length = 0;
inline bool is_embedded() const
{
return offset == 0;
}
};
struct EmbeddedInstruction : Instruction
{
SmallVector<uint32_t> ops;
};
enum Types
{
TypeNone,
TypeType,
TypeVariable,
TypeConstant,
TypeFunction,
TypeFunctionPrototype,
TypeBlock,
TypeExtension,
TypeExpression,
TypeConstantOp,
TypeCombinedImageSampler,
TypeAccessChain,
TypeUndef,
TypeString,
TypeCount
};
template <Types type>
class TypedID;
template <>
class TypedID<TypeNone>
{
public:
TypedID() = default;
TypedID(uint32_t id_)
: id(id_)
{
}
template <Types U>
TypedID(const TypedID<U> &other)
{
*this = other;
}
template <Types U>
TypedID &operator=(const TypedID<U> &other)
{
id = uint32_t(other);
return *this;
}
// Implicit conversion to u32 is desired here.
// As long as we block implicit conversion between TypedID<A> and TypedID<B> we're good.
operator uint32_t() const
{
return id;
}
template <Types U>
operator TypedID<U>() const
{
return TypedID<U>(*this);
}
private:
uint32_t id = 0;
};
template <Types type>
class TypedID
{
public:
TypedID() = default;
TypedID(uint32_t id_)
: id(id_)
{
}
explicit TypedID(const TypedID<TypeNone> &other)
: id(uint32_t(other))
{
}
operator uint32_t() const
{
return id;
}
private:
uint32_t id = 0;
};
using VariableID = TypedID<TypeVariable>;
using TypeID = TypedID<TypeType>;
using ConstantID = TypedID<TypeConstant>;
using FunctionID = TypedID<TypeFunction>;
using BlockID = TypedID<TypeBlock>;
using ID = TypedID<TypeNone>;
// Helper for Variant interface.
struct IVariant
{
virtual ~IVariant() = default;
virtual IVariant *clone(ObjectPoolBase *pool) = 0;
ID self = 0;
protected:
IVariant() = default;
IVariant(const IVariant&) = default;
IVariant &operator=(const IVariant&) = default;
};
#define SPIRV_CROSS_DECLARE_CLONE(T) \
IVariant *clone(ObjectPoolBase *pool) override \
{ \
return static_cast<ObjectPool<T> *>(pool)->allocate(*this); \
}
struct SPIRUndef : IVariant
{
enum
{
type = TypeUndef
};
explicit SPIRUndef(TypeID basetype_)
: basetype(basetype_)
{
}
TypeID basetype;
SPIRV_CROSS_DECLARE_CLONE(SPIRUndef)
};
struct SPIRString : IVariant
{
enum
{
type = TypeString
};
explicit SPIRString(std::string str_)
: str(std::move(str_))
{
}
std::string str;
SPIRV_CROSS_DECLARE_CLONE(SPIRString)
};
// This type is only used by backends which need to access the combined image and sampler IDs separately after
// the OpSampledImage opcode.
struct SPIRCombinedImageSampler : IVariant
{
enum
{
type = TypeCombinedImageSampler
};
SPIRCombinedImageSampler(TypeID type_, VariableID image_, VariableID sampler_)
: combined_type(type_)
, image(image_)
, sampler(sampler_)
{
}
TypeID combined_type;
VariableID image;
VariableID sampler;
SPIRV_CROSS_DECLARE_CLONE(SPIRCombinedImageSampler)
};
struct SPIRConstantOp : IVariant
{
enum
{
type = TypeConstantOp
};
SPIRConstantOp(TypeID result_type, spv::Op op, const uint32_t *args, uint32_t length)
: opcode(op)
, basetype(result_type)
{
arguments.reserve(length);
for (uint32_t i = 0; i < length; i++)
arguments.push_back(args[i]);
}
spv::Op opcode;
SmallVector<uint32_t> arguments;
TypeID basetype;
SPIRV_CROSS_DECLARE_CLONE(SPIRConstantOp)
};
struct SPIRType : IVariant
{
enum
{
type = TypeType
};
spv::Op op = spv::Op::OpNop;
explicit SPIRType(spv::Op op_) : op(op_) {}
enum BaseType
{
Unknown,
Void,
Boolean,
SByte,
UByte,
Short,
UShort,
Int,
UInt,
Int64,
UInt64,
AtomicCounter,
Half,
Float,
Double,
Struct,
Image,
SampledImage,
Sampler,
AccelerationStructure,
RayQuery,
// Keep internal types at the end.
ControlPointArray,
Interpolant,
Char
};
// Scalar/vector/matrix support.
BaseType basetype = Unknown;
uint32_t width = 0;
uint32_t vecsize = 1;
uint32_t columns = 1;
// Arrays, support array of arrays by having a vector of array sizes.
SmallVector<uint32_t> array;
// Array elements can be either specialization constants or specialization ops.
// This array determines how to interpret the array size.
// If an element is true, the element is a literal,
// otherwise, it's an expression, which must be resolved on demand.
// The actual size is not really known until runtime.
SmallVector<bool> array_size_literal;
// Pointers
// Keep track of how many pointer layers we have.
uint32_t pointer_depth = 0;
bool pointer = false;
bool forward_pointer = false;
spv::StorageClass storage = spv::StorageClassGeneric;
SmallVector<TypeID> member_types;
// If member order has been rewritten to handle certain scenarios with Offset,
// allow codegen to rewrite the index.
SmallVector<uint32_t> member_type_index_redirection;
struct ImageType
{
TypeID type;
spv::Dim dim;
bool depth;
bool arrayed;
bool ms;
uint32_t sampled;
spv::ImageFormat format;
spv::AccessQualifier access;
} image = {};
// Structs can be declared multiple times if they are used as part of interface blocks.
// We want to detect this so that we only emit the struct definition once.
// Since we cannot rely on OpName to be equal, we need to figure out aliases.
TypeID type_alias = 0;
// Denotes the type which this type is based on.
// Allows the backend to traverse how a complex type is built up during access chains.
TypeID parent_type = 0;
// Used in backends to avoid emitting members with conflicting names.
std::unordered_set<std::string> member_name_cache;
SPIRV_CROSS_DECLARE_CLONE(SPIRType)
};
struct SPIRExtension : IVariant
{
enum
{
type = TypeExtension
};
enum Extension
{
Unsupported,
GLSL,
SPV_debug_info,
SPV_AMD_shader_ballot,
SPV_AMD_shader_explicit_vertex_parameter,
SPV_AMD_shader_trinary_minmax,
SPV_AMD_gcn_shader,
NonSemanticDebugPrintf,
NonSemanticShaderDebugInfo,
NonSemanticGeneric
};
explicit SPIRExtension(Extension ext_)
: ext(ext_)
{
}
Extension ext;
SPIRV_CROSS_DECLARE_CLONE(SPIRExtension)
};
// SPIREntryPoint is not a variant since its IDs are used to decorate OpFunction,
// so in order to avoid conflicts, we can't stick them in the ids array.
struct SPIREntryPoint
{
SPIREntryPoint(FunctionID self_, spv::ExecutionModel execution_model, const std::string &entry_name)
: self(self_)
, name(entry_name)
, orig_name(entry_name)
, model(execution_model)
{
}
SPIREntryPoint() = default;
FunctionID self = 0;
std::string name;
std::string orig_name;
SmallVector<VariableID> interface_variables;
Bitset flags;
struct WorkgroupSize
{
uint32_t x = 0, y = 0, z = 0;
uint32_t id_x = 0, id_y = 0, id_z = 0;
uint32_t constant = 0; // Workgroup size can be expressed as a constant/spec-constant instead.
} workgroup_size;
uint32_t invocations = 0;
uint32_t output_vertices = 0;
uint32_t output_primitives = 0;
spv::ExecutionModel model = spv::ExecutionModelMax;
bool geometry_passthrough = false;
};
struct SPIRExpression : IVariant
{
enum
{
type = TypeExpression
};
// Only created by the backend target to avoid creating tons of temporaries.
SPIRExpression(std::string expr, TypeID expression_type_, bool immutable_)
: expression(std::move(expr))
, expression_type(expression_type_)
, immutable(immutable_)
{
}
// If non-zero, prepend expression with to_expression(base_expression).
// Used in amortizing multiple calls to to_expression()
// where in certain cases that would quickly force a temporary when not needed.
ID base_expression = 0;
std::string expression;
TypeID expression_type = 0;
// If this expression is a forwarded load,
// allow us to reference the original variable.
ID loaded_from = 0;
// If this expression will never change, we can avoid lots of temporaries
// in high level source.
// An expression being immutable can be speculative,
// it is assumed that this is true almost always.
bool immutable = false;
// Before use, this expression must be transposed.
// This is needed for targets which don't support row_major layouts.
bool need_transpose = false;
// Whether or not this is an access chain expression.
bool access_chain = false;
// Whether or not gl_MeshVerticesEXT[].gl_Position (as a whole or .y) is referenced
bool access_meshlet_position_y = false;
// A list of expressions which this expression depends on.
SmallVector<ID> expression_dependencies;
// By reading this expression, we implicitly read these expressions as well.
// Used by access chain Store and Load since we read multiple expressions in this case.
SmallVector<ID> implied_read_expressions;
// The expression was emitted at a certain scope. Lets us track when an expression read means multiple reads.
uint32_t emitted_loop_level = 0;
SPIRV_CROSS_DECLARE_CLONE(SPIRExpression)
};
struct SPIRFunctionPrototype : IVariant
{
enum
{
type = TypeFunctionPrototype
};
explicit SPIRFunctionPrototype(TypeID return_type_)
: return_type(return_type_)
{
}
TypeID return_type;
SmallVector<uint32_t> parameter_types;
SPIRV_CROSS_DECLARE_CLONE(SPIRFunctionPrototype)
};
struct SPIRBlock : IVariant
{
enum
{
type = TypeBlock
};
enum Terminator
{
Unknown,
Direct, // Emit next block directly without a particular condition.
Select, // Block ends with an if/else block.
MultiSelect, // Block ends with switch statement.
Return, // Block ends with return.
Unreachable, // Noop
Kill, // Discard
IgnoreIntersection, // Ray Tracing
TerminateRay, // Ray Tracing
EmitMeshTasks // Mesh shaders
};
enum Merge
{
MergeNone,
MergeLoop,
MergeSelection
};
enum Hints
{
HintNone,
HintUnroll,
HintDontUnroll,
HintFlatten,
HintDontFlatten
};
enum Method
{
MergeToSelectForLoop,
MergeToDirectForLoop,
MergeToSelectContinueForLoop
};
enum ContinueBlockType
{
ContinueNone,
// Continue block is branchless and has at least one instruction.
ForLoop,
// Noop continue block.
WhileLoop,
// Continue block is conditional.
DoWhileLoop,
// Highly unlikely that anything will use this,
// since it is really awkward/impossible to express in GLSL.
ComplexLoop
};
enum : uint32_t
{
NoDominator = 0xffffffffu
};
Terminator terminator = Unknown;
Merge merge = MergeNone;
Hints hint = HintNone;
BlockID next_block = 0;
BlockID merge_block = 0;
BlockID continue_block = 0;
ID return_value = 0; // If 0, return nothing (void).
ID condition = 0;
BlockID true_block = 0;
BlockID false_block = 0;
BlockID default_block = 0;
// If terminator is EmitMeshTasksEXT.
struct
{
ID groups[3];
ID payload;
} mesh = {};
SmallVector<Instruction> ops;
struct Phi
{
ID local_variable; // flush local variable ...
BlockID parent; // If we're in from_block and want to branch into this block ...
VariableID function_variable; // to this function-global "phi" variable first.
};
// Before entering this block flush out local variables to magical "phi" variables.
SmallVector<Phi> phi_variables;
// Declare these temporaries before beginning the block.
// Used for handling complex continue blocks which have side effects.
SmallVector<std::pair<TypeID, ID>> declare_temporary;
// Declare these temporaries, but only conditionally if this block turns out to be
// a complex loop header.
SmallVector<std::pair<TypeID, ID>> potential_declare_temporary;
struct Case
{
uint64_t value;
BlockID block;
};
SmallVector<Case> cases_32bit;
SmallVector<Case> cases_64bit;
// If we have tried to optimize code for this block but failed,
// keep track of this.
bool disable_block_optimization = false;
// If the continue block is complex, fallback to "dumb" for loops.
bool complex_continue = false;
// Do we need a ladder variable to defer breaking out of a loop construct after a switch block?
bool need_ladder_break = false;
// If marked, we have explicitly handled Phi from this block, so skip any flushes related to that on a branch.
// Used to handle an edge case with switch and case-label fallthrough where fall-through writes to Phi.
BlockID ignore_phi_from_block = 0;
// The dominating block which this block might be within.
// Used in continue; blocks to determine if we really need to write continue.
BlockID loop_dominator = 0;
// All access to these variables are dominated by this block,
// so before branching anywhere we need to make sure that we declare these variables.
SmallVector<VariableID> dominated_variables;
// These are variables which should be declared in a for loop header, if we
// fail to use a classic for-loop,
// we remove these variables, and fall back to regular variables outside the loop.
SmallVector<VariableID> loop_variables;
// Some expressions are control-flow dependent, i.e. any instruction which relies on derivatives or
// sub-group-like operations.
// Make sure that we only use these expressions in the original block.
SmallVector<ID> invalidate_expressions;
SPIRV_CROSS_DECLARE_CLONE(SPIRBlock)
};
struct SPIRFunction : IVariant
{
enum
{
type = TypeFunction
};
SPIRFunction(TypeID return_type_, TypeID function_type_)
: return_type(return_type_)
, function_type(function_type_)
{
}
struct Parameter
{
TypeID type;
ID id;
uint32_t read_count;
uint32_t write_count;
// Set to true if this parameter aliases a global variable,
// used mostly in Metal where global variables
// have to be passed down to functions as regular arguments.
// However, for this kind of variable, we should not care about
// read and write counts as access to the function arguments
// is not local to the function in question.
bool alias_global_variable;
};
// When calling a function, and we're remapping separate image samplers,
// resolve these arguments into combined image samplers and pass them
// as additional arguments in this order.
// It gets more complicated as functions can pull in their own globals
// and combine them with parameters,
// so we need to distinguish if something is local parameter index
// or a global ID.
struct CombinedImageSamplerParameter
{
VariableID id;
VariableID image_id;
VariableID sampler_id;
bool global_image;
bool global_sampler;
bool depth;
};
TypeID return_type;
TypeID function_type;
SmallVector<Parameter> arguments;
// Can be used by backends to add magic arguments.
// Currently used by combined image/sampler implementation.
SmallVector<Parameter> shadow_arguments;
SmallVector<VariableID> local_variables;
BlockID entry_block = 0;
SmallVector<BlockID> blocks;
SmallVector<CombinedImageSamplerParameter> combined_parameters;
struct EntryLine
{
uint32_t file_id = 0;
uint32_t line_literal = 0;
};
EntryLine entry_line;
void add_local_variable(VariableID id)
{
local_variables.push_back(id);
}
void add_parameter(TypeID parameter_type, ID id, bool alias_global_variable = false)
{
// Arguments are read-only until proven otherwise.
arguments.push_back({ parameter_type, id, 0u, 0u, alias_global_variable });
}
// Hooks to be run when the function returns.
// Mostly used for lowering internal data structures onto flattened structures.
// Need to defer this, because they might rely on things which change during compilation.
// Intentionally not a small vector, this one is rare, and std::function can be large.
Vector<std::function<void()>> fixup_hooks_out;
// Hooks to be run when the function begins.
// Mostly used for populating internal data structures from flattened structures.
// Need to defer this, because they might rely on things which change during compilation.
// Intentionally not a small vector, this one is rare, and std::function can be large.
Vector<std::function<void()>> fixup_hooks_in;
// On function entry, make sure to copy a constant array into thread addr space to work around
// the case where we are passing a constant array by value to a function on backends which do not
// consider arrays value types.
SmallVector<ID> constant_arrays_needed_on_stack;
bool active = false;
bool flush_undeclared = true;
bool do_combined_parameters = true;
SPIRV_CROSS_DECLARE_CLONE(SPIRFunction)
};
struct SPIRAccessChain : IVariant
{
enum
{
type = TypeAccessChain
};
SPIRAccessChain(TypeID basetype_, spv::StorageClass storage_, std::string base_, std::string dynamic_index_,
int32_t static_index_)
: basetype(basetype_)
, storage(storage_)
, base(std::move(base_))
, dynamic_index(std::move(dynamic_index_))
, static_index(static_index_)
{
}
// The access chain represents an offset into a buffer.
// Some backends need more complicated handling of access chains to be able to use buffers, like HLSL
// which has no usable buffer type ala GLSL SSBOs.
// StructuredBuffer is too limited, so our only option is to deal with ByteAddressBuffer which works with raw addresses.
TypeID basetype;
spv::StorageClass storage;
std::string base;
std::string dynamic_index;
int32_t static_index;
VariableID loaded_from = 0;
uint32_t matrix_stride = 0;
uint32_t array_stride = 0;
bool row_major_matrix = false;
bool immutable = false;
// By reading this expression, we implicitly read these expressions as well.
// Used by access chain Store and Load since we read multiple expressions in this case.
SmallVector<ID> implied_read_expressions;
SPIRV_CROSS_DECLARE_CLONE(SPIRAccessChain)
};
struct SPIRVariable : IVariant
{
enum
{
type = TypeVariable
};
SPIRVariable() = default;
SPIRVariable(TypeID basetype_, spv::StorageClass storage_, ID initializer_ = 0, VariableID basevariable_ = 0)
: basetype(basetype_)
, storage(storage_)
, initializer(initializer_)
, basevariable(basevariable_)
{
}
TypeID basetype = 0;
spv::StorageClass storage = spv::StorageClassGeneric;
uint32_t decoration = 0;
ID initializer = 0;
VariableID basevariable = 0;
SmallVector<uint32_t> dereference_chain;
bool compat_builtin = false;
// If a variable is shadowed, we only statically assign to it
// and never actually emit a statement for it.
// When we read the variable as an expression, just forward
// shadowed_id as the expression.
bool statically_assigned = false;
ID static_expression = 0;
// Temporaries which can remain forwarded as long as this variable is not modified.
SmallVector<ID> dependees;
bool deferred_declaration = false;
bool phi_variable = false;
// Used to deal with Phi variable flushes. See flush_phi().
bool allocate_temporary_copy = false;
bool remapped_variable = false;
uint32_t remapped_components = 0;
// The block which dominates all access to this variable.
BlockID dominator = 0;
// If true, this variable is a loop variable, when accessing the variable
// outside a loop,
// we should statically forward it.
bool loop_variable = false;
// Set to true while we're inside the for loop.
bool loop_variable_enable = false;
// Used to find global LUTs
bool is_written_to = false;
SPIRFunction::Parameter *parameter = nullptr;
SPIRV_CROSS_DECLARE_CLONE(SPIRVariable)
};
struct SPIRConstant : IVariant
{
enum
{
type = TypeConstant
};
union Constant
{
uint32_t u32;
int32_t i32;
float f32;
uint64_t u64;
int64_t i64;
double f64;
};
struct ConstantVector
{
Constant r[4];
// If != 0, this element is a specialization constant, and we should keep track of it as such.
ID id[4];
uint32_t vecsize = 1;
ConstantVector()
{
memset(r, 0, sizeof(r));
}
};
struct ConstantMatrix
{
ConstantVector c[4];
// If != 0, this column is a specialization constant, and we should keep track of it as such.
ID id[4];
uint32_t columns = 1;
};
static inline float f16_to_f32(uint16_t u16_value)
{
// Based on the GLM implementation.
int s = (u16_value >> 15) & 0x1;
int e = (u16_value >> 10) & 0x1f;
int m = (u16_value >> 0) & 0x3ff;
union
{
float f32;
uint32_t u32;
} u;
if (e == 0)
{
if (m == 0)
{
u.u32 = uint32_t(s) << 31;
return u.f32;
}
else
{
while ((m & 0x400) == 0)
{
m <<= 1;
e--;
}
e++;
m &= ~0x400;
}
}
else if (e == 31)
{
if (m == 0)
{
u.u32 = (uint32_t(s) << 31) | 0x7f800000u;
return u.f32;
}
else
{
u.u32 = (uint32_t(s) << 31) | 0x7f800000u | (m << 13);
return u.f32;
}
}
e += 127 - 15;
m <<= 13;
u.u32 = (uint32_t(s) << 31) | (e << 23) | m;
return u.f32;
}
inline uint32_t specialization_constant_id(uint32_t col, uint32_t row) const
{
return m.c[col].id[row];
}
inline uint32_t specialization_constant_id(uint32_t col) const
{
return m.id[col];
}
inline uint32_t scalar(uint32_t col = 0, uint32_t row = 0) const
{
return m.c[col].r[row].u32;
}
inline int16_t scalar_i16(uint32_t col = 0, uint32_t row = 0) const
{
return int16_t(m.c[col].r[row].u32 & 0xffffu);
}
inline uint16_t scalar_u16(uint32_t col = 0, uint32_t row = 0) const
{
return uint16_t(m.c[col].r[row].u32 & 0xffffu);
}
inline int8_t scalar_i8(uint32_t col = 0, uint32_t row = 0) const
{
return int8_t(m.c[col].r[row].u32 & 0xffu);
}
inline uint8_t scalar_u8(uint32_t col = 0, uint32_t row = 0) const
{
return uint8_t(m.c[col].r[row].u32 & 0xffu);
}
inline float scalar_f16(uint32_t col = 0, uint32_t row = 0) const
{
return f16_to_f32(scalar_u16(col, row));
}
inline float scalar_f32(uint32_t col = 0, uint32_t row = 0) const
{
return m.c[col].r[row].f32;
}
inline int32_t scalar_i32(uint32_t col = 0, uint32_t row = 0) const
{
return m.c[col].r[row].i32;
}
inline double scalar_f64(uint32_t col = 0, uint32_t row = 0) const
{
return m.c[col].r[row].f64;
}
inline int64_t scalar_i64(uint32_t col = 0, uint32_t row = 0) const
{
return m.c[col].r[row].i64;
}
inline uint64_t scalar_u64(uint32_t col = 0, uint32_t row = 0) const
{
return m.c[col].r[row].u64;
}
inline const ConstantVector &vector() const
{
return m.c[0];
}
inline uint32_t vector_size() const
{
return m.c[0].vecsize;
}
inline uint32_t columns() const
{
return m.columns;
}
inline void make_null(const SPIRType &constant_type_)
{
m = {};
m.columns = constant_type_.columns;
for (auto &c : m.c)
c.vecsize = constant_type_.vecsize;
}
inline bool constant_is_null() const
{
if (specialization)
return false;
if (!subconstants.empty())
return false;
for (uint32_t col = 0; col < columns(); col++)
for (uint32_t row = 0; row < vector_size(); row++)
if (scalar_u64(col, row) != 0)
return false;
return true;
}
explicit SPIRConstant(uint32_t constant_type_)
: constant_type(constant_type_)
{
}
SPIRConstant() = default;
SPIRConstant(TypeID constant_type_, const uint32_t *elements, uint32_t num_elements, bool specialized)
: constant_type(constant_type_)
, specialization(specialized)
{
subconstants.reserve(num_elements);
for (uint32_t i = 0; i < num_elements; i++)
subconstants.push_back(elements[i]);
specialization = specialized;
}
// Construct scalar (32-bit).
SPIRConstant(TypeID constant_type_, uint32_t v0, bool specialized)
: constant_type(constant_type_)
, specialization(specialized)
{
m.c[0].r[0].u32 = v0;
m.c[0].vecsize = 1;
m.columns = 1;
}
// Construct scalar (64-bit).
SPIRConstant(TypeID constant_type_, uint64_t v0, bool specialized)
: constant_type(constant_type_)
, specialization(specialized)
{
m.c[0].r[0].u64 = v0;
m.c[0].vecsize = 1;
m.columns = 1;
}
// Construct vectors and matrices.
SPIRConstant(TypeID constant_type_, const SPIRConstant *const *vector_elements, uint32_t num_elements,
bool specialized)
: constant_type(constant_type_)
, specialization(specialized)
{
bool matrix = vector_elements[0]->m.c[0].vecsize > 1;
if (matrix)
{
m.columns = num_elements;
for (uint32_t i = 0; i < num_elements; i++)
{
m.c[i] = vector_elements[i]->m.c[0];
if (vector_elements[i]->specialization)
m.id[i] = vector_elements[i]->self;
}
}
else
{
m.c[0].vecsize = num_elements;
m.columns = 1;
for (uint32_t i = 0; i < num_elements; i++)
{
m.c[0].r[i] = vector_elements[i]->m.c[0].r[0];
if (vector_elements[i]->specialization)
m.c[0].id[i] = vector_elements[i]->self;
}
}
}
TypeID constant_type = 0;
ConstantMatrix m;
// If this constant is a specialization constant (i.e. created with OpSpecConstant*).
bool specialization = false;
// If this constant is used as an array length which creates specialization restrictions on some backends.
bool is_used_as_array_length = false;
// If true, this is a LUT, and should always be declared in the outer scope.
bool is_used_as_lut = false;
// For composites which are constant arrays, etc.
SmallVector<ConstantID> subconstants;
// Non-Vulkan GLSL, HLSL and sometimes MSL emits defines for each specialization constant,
// and uses them to initialize the constant. This allows the user
// to still be able to specialize the value by supplying corresponding
// preprocessor directives before compiling the shader.
std::string specialization_constant_macro_name;
SPIRV_CROSS_DECLARE_CLONE(SPIRConstant)
};
// Variants have a very specific allocation scheme.
struct ObjectPoolGroup
{
std::unique_ptr<ObjectPoolBase> pools[TypeCount];
};
class Variant
{
public:
explicit Variant(ObjectPoolGroup *group_)
: group(group_)
{
}
~Variant()
{
if (holder)
group->pools[type]->deallocate_opaque(holder);
}
// Marking custom move constructor as noexcept is important.
Variant(Variant &&other) SPIRV_CROSS_NOEXCEPT
{
*this = std::move(other);
}
// We cannot copy from other variant without our own pool group.
// Have to explicitly copy.
Variant(const Variant &variant) = delete;
// Marking custom move constructor as noexcept is important.
Variant &operator=(Variant &&other) SPIRV_CROSS_NOEXCEPT
{
if (this != &other)
{
if (holder)
group->pools[type]->deallocate_opaque(holder);
holder = other.holder;
group = other.group;
type = other.type;
allow_type_rewrite = other.allow_type_rewrite;
other.holder = nullptr;
other.type = TypeNone;
}
return *this;
}
// This copy/clone should only be called in the Compiler constructor.
// If this is called inside ::compile(), we invalidate any references we took higher in the stack.
// This should never happen.
Variant &operator=(const Variant &other)
{
//#define SPIRV_CROSS_COPY_CONSTRUCTOR_SANITIZE
#ifdef SPIRV_CROSS_COPY_CONSTRUCTOR_SANITIZE
abort();
#endif
if (this != &other)
{
if (holder)
group->pools[type]->deallocate_opaque(holder);
if (other.holder)
holder = other.holder->clone(group->pools[other.type].get());
else
holder = nullptr;
type = other.type;
allow_type_rewrite = other.allow_type_rewrite;
}
return *this;
}
void set(IVariant *val, Types new_type)
{
if (holder)
group->pools[type]->deallocate_opaque(holder);
holder = nullptr;
if (!allow_type_rewrite && type != TypeNone && type != new_type)
{
if (val)
group->pools[new_type]->deallocate_opaque(val);
SPIRV_CROSS_THROW("Overwriting a variant with new type.");
}
holder = val;
type = new_type;
allow_type_rewrite = false;
}
template <typename T, typename... Ts>
T *allocate_and_set(Types new_type, Ts &&... ts)
{
T *val = static_cast<ObjectPool<T> &>(*group->pools[new_type]).allocate(std::forward<Ts>(ts)...);
set(val, new_type);
return val;
}
template <typename T>
T &get()
{
if (!holder)
SPIRV_CROSS_THROW("nullptr");
if (static_cast<Types>(T::type) != type)
SPIRV_CROSS_THROW("Bad cast");
return *static_cast<T *>(holder);
}
template <typename T>
const T &get() const
{
if (!holder)
SPIRV_CROSS_THROW("nullptr");
if (static_cast<Types>(T::type) != type)
SPIRV_CROSS_THROW("Bad cast");
return *static_cast<const T *>(holder);
}
Types get_type() const
{
return type;
}
ID get_id() const
{
return holder ? holder->self : ID(0);
}
bool empty() const
{
return !holder;
}
void reset()
{
if (holder)
group->pools[type]->deallocate_opaque(holder);
holder = nullptr;
type = TypeNone;
}
void set_allow_type_rewrite()
{
allow_type_rewrite = true;
}
private:
ObjectPoolGroup *group = nullptr;
IVariant *holder = nullptr;
Types type = TypeNone;
bool allow_type_rewrite = false;
};
template <typename T>
T &variant_get(Variant &var)
{
return var.get<T>();
}
template <typename T>
const T &variant_get(const Variant &var)
{
return var.get<T>();
}
template <typename T, typename... P>
T &variant_set(Variant &var, P &&... args)
{
auto *ptr = var.allocate_and_set<T>(static_cast<Types>(T::type), std::forward<P>(args)...);
return *ptr;
}
struct AccessChainMeta
{
uint32_t storage_physical_type = 0;
bool need_transpose = false;
bool storage_is_packed = false;
bool storage_is_invariant = false;
bool flattened_struct = false;
bool relaxed_precision = false;
bool access_meshlet_position_y = false;
};
enum ExtendedDecorations
{
// Marks if a buffer block is re-packed, i.e. member declaration might be subject to PhysicalTypeID remapping and padding.
SPIRVCrossDecorationBufferBlockRepacked = 0,
// A type in a buffer block might be declared with a different physical type than the logical type.
// If this is not set, PhysicalTypeID == the SPIR-V type as declared.
SPIRVCrossDecorationPhysicalTypeID,
// Marks if the physical type is to be declared with tight packing rules, i.e. packed_floatN on MSL and friends.
// If this is set, PhysicalTypeID might also be set. It can be set to same as logical type if all we're doing
// is converting float3 to packed_float3 for example.
// If this is marked on a struct, it means the struct itself must use only Packed types for all its members.
SPIRVCrossDecorationPhysicalTypePacked,
// The padding in bytes before declaring this struct member.
// If used on a struct type, marks the target size of a struct.
SPIRVCrossDecorationPaddingTarget,
SPIRVCrossDecorationInterfaceMemberIndex,
SPIRVCrossDecorationInterfaceOrigID,
SPIRVCrossDecorationResourceIndexPrimary,
// Used for decorations like resource indices for samplers when part of combined image samplers.
// A variable might need to hold two resource indices in this case.
SPIRVCrossDecorationResourceIndexSecondary,
// Used for resource indices for multiplanar images when part of combined image samplers.
SPIRVCrossDecorationResourceIndexTertiary,
SPIRVCrossDecorationResourceIndexQuaternary,
// Marks a buffer block for using explicit offsets (GLSL/HLSL).
SPIRVCrossDecorationExplicitOffset,
// Apply to a variable in the Input storage class; marks it as holding the base group passed to vkCmdDispatchBase(),
// or the base vertex and instance indices passed to vkCmdDrawIndexed().
// In MSL, this is used to adjust the WorkgroupId and GlobalInvocationId variables in compute shaders,
// and to hold the BaseVertex and BaseInstance variables in vertex shaders.
SPIRVCrossDecorationBuiltInDispatchBase,
// Apply to a variable that is a function parameter; marks it as being a "dynamic"
// combined image-sampler. In MSL, this is used when a function parameter might hold
// either a regular combined image-sampler or one that has an attached sampler
// Y'CbCr conversion.
SPIRVCrossDecorationDynamicImageSampler,
// Apply to a variable in the Input storage class; marks it as holding the size of the stage
// input grid.
// In MSL, this is used to hold the vertex and instance counts in a tessellation pipeline
// vertex shader.
SPIRVCrossDecorationBuiltInStageInputSize,
// Apply to any access chain of a tessellation I/O variable; stores the type of the sub-object
// that was chained to, as recorded in the input variable itself. This is used in case the pointer
// is itself used as the base of an access chain, to calculate the original type of the sub-object
// chained to, in case a swizzle needs to be applied. This should not happen normally with valid
// SPIR-V, but the MSL backend can change the type of input variables, necessitating the
// addition of swizzles to keep the generated code compiling.
SPIRVCrossDecorationTessIOOriginalInputTypeID,
// Apply to any access chain of an interface variable used with pull-model interpolation, where the variable is a
// vector but the resulting pointer is a scalar; stores the component index that is to be accessed by the chain.
// This is used when emitting calls to interpolation functions on the chain in MSL: in this case, the component
// must be applied to the result, since pull-model interpolants in MSL cannot be swizzled directly, but the
// results of interpolation can.
SPIRVCrossDecorationInterpolantComponentExpr,
// Apply to any struct type that is used in the Workgroup storage class.
// This causes matrices in MSL prior to Metal 3.0 to be emitted using a special
// class that is convertible to the standard matrix type, to work around the
// lack of constructors in the 'threadgroup' address space.
SPIRVCrossDecorationWorkgroupStruct,
SPIRVCrossDecorationOverlappingBinding,
SPIRVCrossDecorationCount
};
struct Meta
{
struct Decoration
{
std::string alias;
std::string qualified_alias;
std::string hlsl_semantic;
std::string user_type;
Bitset decoration_flags;
spv::BuiltIn builtin_type = spv::BuiltInMax;
uint32_t location = 0;
uint32_t component = 0;
uint32_t set = 0;
uint32_t binding = 0;
uint32_t offset = 0;
uint32_t xfb_buffer = 0;
uint32_t xfb_stride = 0;
uint32_t stream = 0;
uint32_t array_stride = 0;
uint32_t matrix_stride = 0;
uint32_t input_attachment = 0;
uint32_t spec_id = 0;
uint32_t index = 0;
spv::FPRoundingMode fp_rounding_mode = spv::FPRoundingModeMax;
bool builtin = false;
bool qualified_alias_explicit_override = false;
struct Extended
{
Extended()
{
// MSVC 2013 workaround to init like this.
for (auto &v : values)
v = 0;
}
Bitset flags;
uint32_t values[SPIRVCrossDecorationCount];
} extended;
};
Decoration decoration;
// Intentionally not a SmallVector. Decoration is large and somewhat rare.
Vector<Decoration> members;
std::unordered_map<uint32_t, uint32_t> decoration_word_offset;
// For SPV_GOOGLE_hlsl_functionality1.
bool hlsl_is_magic_counter_buffer = false;
// ID for the sibling counter buffer.
uint32_t hlsl_magic_counter_buffer = 0;
};
// A user callback that remaps the type of any variable.
// var_name is the declared name of the variable.
// name_of_type is the textual name of the type which will be used in the code unless written to by the callback.
using VariableTypeRemapCallback =
std::function<void(const SPIRType &type, const std::string &var_name, std::string &name_of_type)>;
class Hasher
{
public:
inline void u32(uint32_t value)
{
h = (h * 0x100000001b3ull) ^ value;
}
inline uint64_t get() const
{
return h;
}
private:
uint64_t h = 0xcbf29ce484222325ull;
};
static inline bool type_is_floating_point(const SPIRType &type)
{
return type.basetype == SPIRType::Half || type.basetype == SPIRType::Float || type.basetype == SPIRType::Double;
}
static inline bool type_is_integral(const SPIRType &type)
{
return type.basetype == SPIRType::SByte || type.basetype == SPIRType::UByte || type.basetype == SPIRType::Short ||
type.basetype == SPIRType::UShort || type.basetype == SPIRType::Int || type.basetype == SPIRType::UInt ||
type.basetype == SPIRType::Int64 || type.basetype == SPIRType::UInt64;
}
static inline SPIRType::BaseType to_signed_basetype(uint32_t width)
{
switch (width)
{
case 8:
return SPIRType::SByte;
case 16:
return SPIRType::Short;
case 32:
return SPIRType::Int;
case 64:
return SPIRType::Int64;
default:
SPIRV_CROSS_THROW("Invalid bit width.");
}
}
static inline SPIRType::BaseType to_unsigned_basetype(uint32_t width)
{
switch (width)
{
case 8:
return SPIRType::UByte;
case 16:
return SPIRType::UShort;
case 32:
return SPIRType::UInt;
case 64:
return SPIRType::UInt64;
default:
SPIRV_CROSS_THROW("Invalid bit width.");
}
}
// Returns true if an arithmetic operation does not change behavior depending on signedness.
static inline bool opcode_is_sign_invariant(spv::Op opcode)
{
switch (opcode)
{
case spv::OpIEqual:
case spv::OpINotEqual:
case spv::OpISub:
case spv::OpIAdd:
case spv::OpIMul:
case spv::OpShiftLeftLogical:
case spv::OpBitwiseOr:
case spv::OpBitwiseXor:
case spv::OpBitwiseAnd:
return true;
default:
return false;
}
}
static inline bool opcode_can_promote_integer_implicitly(spv::Op opcode)
{
switch (opcode)
{
case spv::OpSNegate:
case spv::OpNot:
case spv::OpBitwiseAnd:
case spv::OpBitwiseOr:
case spv::OpBitwiseXor:
case spv::OpShiftLeftLogical:
case spv::OpShiftRightLogical:
case spv::OpShiftRightArithmetic:
case spv::OpIAdd:
case spv::OpISub:
case spv::OpIMul:
case spv::OpSDiv:
case spv::OpUDiv:
case spv::OpSRem:
case spv::OpUMod:
case spv::OpSMod:
return true;
default:
return false;
}
}
struct SetBindingPair
{
uint32_t desc_set;
uint32_t binding;
inline bool operator==(const SetBindingPair &other) const
{
return desc_set == other.desc_set && binding == other.binding;
}
inline bool operator<(const SetBindingPair &other) const
{
return desc_set < other.desc_set || (desc_set == other.desc_set && binding < other.binding);
}
};
struct LocationComponentPair
{
uint32_t location;
uint32_t component;
inline bool operator==(const LocationComponentPair &other) const
{
return location == other.location && component == other.component;
}
inline bool operator<(const LocationComponentPair &other) const
{
return location < other.location || (location == other.location && component < other.component);
}
};
struct StageSetBinding
{
spv::ExecutionModel model;
uint32_t desc_set;
uint32_t binding;
inline bool operator==(const StageSetBinding &other) const
{
return model == other.model && desc_set == other.desc_set && binding == other.binding;
}
};
struct InternalHasher
{
inline size_t operator()(const SetBindingPair &value) const
{
// Quality of hash doesn't really matter here.
auto hash_set = std::hash<uint32_t>()(value.desc_set);
auto hash_binding = std::hash<uint32_t>()(value.binding);
return (hash_set * 0x10001b31) ^ hash_binding;
}
inline size_t operator()(const LocationComponentPair &value) const
{
// Quality of hash doesn't really matter here.
auto hash_set = std::hash<uint32_t>()(value.location);
auto hash_binding = std::hash<uint32_t>()(value.component);
return (hash_set * 0x10001b31) ^ hash_binding;
}
inline size_t operator()(const StageSetBinding &value) const
{
// Quality of hash doesn't really matter here.
auto hash_model = std::hash<uint32_t>()(value.model);
auto hash_set = std::hash<uint32_t>()(value.desc_set);
auto tmp_hash = (hash_model * 0x10001b31) ^ hash_set;
return (tmp_hash * 0x10001b31) ^ value.binding;
}
};
// Special constant used in a {MSL,HLSL}ResourceBinding desc_set
// element to indicate the bindings for the push constants.
static const uint32_t ResourceBindingPushConstantDescriptorSet = ~(0u);
// Special constant used in a {MSL,HLSL}ResourceBinding binding
// element to indicate the bindings for the push constants.
static const uint32_t ResourceBindingPushConstantBinding = 0;
} // namespace SPIRV_CROSS_NAMESPACE
namespace std
{
template <SPIRV_CROSS_NAMESPACE::Types type>
struct hash<SPIRV_CROSS_NAMESPACE::TypedID<type>>
{
size_t operator()(const SPIRV_CROSS_NAMESPACE::TypedID<type> &value) const
{
return std::hash<uint32_t>()(value);
}
};
} // namespace std
#endif