The IR can be extended by the various backends as needed. This can be done through custom transforms, capabilities, types, intrinsics or through subclasses of other values.
In a few cases we need to make transforms on the IR which are specific to a given backend. In these cases the transforms live in the writer/raise
folder for the backend (e.g. lang/msl/writer/raise/
). These transforms are then added into the raise.cc
file to run the transform when necessary.
Each transform defines its own configuration if necessary. This is typically done by defining a struct
in the transform .h
file with a naming matching the transform with a Config
suffix. (e.g. ModuleConstant
transform has a ModuleConstantConfig
structure). When the transform is called in raise
the configuration will be created from the generator configuration.
Transforms have a set of Capabilities
they support. Often the list of supported capabilities is added to the .h
file so they can be shared with a fuzzer for the transform. The set of capabilities is typically named k
+ transform name + Capabilities
(e.g. ModuleConstant
has kModuleConstantCapabilities
). The set of capabilities should be kept as minimal as possible for the transform. The capabilities are used to control what extra features the IR validator will allow.
Transforms all follow a similar pattern of a free function named after the transform and then a State
struct with a Process
method. The free function will run the validator if necessary and then call the State::Process
method.
Each backend has a .def
file listing the intrinsics for that backend (e.g. lang/msl/msl.def
). These custom intrinsics can be types or instructions. See intrinsic_definition_files.md.
The intrinsic instructions typically match the signature of the backend, so may take values which don‘t make sense coming from core IR or WGSL. (For instance, the GroupNonUniform
SPIR-V instructions take a scope
parameter which doesn’t exist, and isn't used, but is there to match the API on the SPIR-V side.
The naming of the intrinsic is set to match the casing of the backend instruction. So, group_non_uniform_s_min
turns into spirv::BuiltinFn::kGroupNonUniformSMin
which matches to the OpGroupNonUniformSMin
instruction in SPIR-V.
As instructions are added they need to be added to the printer.cc
file for the backend in order to emit the intrinsic. The instruction also needs to be added to the builtin_fn.cc.tmpl
file for the backend to add to the GetSideEffects
switch. This lists if there are side effects to the instruction which need to be accounted for when doing instruction inlining.
When these instructions are added to the IR they are done using the BuiltinCall
class for that backend. For example, to call the kGroupNonUniformBroadcast
intrinsic in the SPIR-V backend we call:
b_.Call<spirv::ir::BuiltinCall>(ty.u32(), spirv::BuiltinFn::kGroupNonUniformBroadcast, Vector{id}))
There are a few places to modify to create a new type. First, create the type .h
and .cc
files in the backend type/
folder. The type will inherit from CastableBase<NewType, Type>
where NewType
is the name of the new type. At a minimum the class needs to override:
bool Equals(const UniqueNode& other) const override; /// @returns the friendly name for this type std::string FriendlyName() const override; /// @param ctx the clone context /// @returns a clone of this type NewType* Clone(core::type::CloneContext& ctx) const override;
The class can also override:
/// @returns the size in bytes of the type. This may include tail padding. /// @note opaque types will return a size of 0. virtual uint32_t Size() const; /// @returns the alignment in bytes of the type. This may include tail /// padding. /// @note opaque types will return a size of 0. virtual uint32_t Align() const;
The Align
and Size
overrides are only necessary if the default of size 0 and align 0 do not work for the new type.
The type can then be added to the .def
file for the backend as type new_type
. In order to use the type in the type matches the type_matches.h
file for the backend needs to be updated to have a MatchNewType
method and a BuildNewType
method. The arguments end up matching the values passed to the type implicit template parameters.
The new types can be created in the type manager by calling ty.Get<spirv::types::NewType>()
, providing any needed arguments for the type constructor.
When defining custom enums in a given backend an extra attribute is needed to set the correct namespace for the enum, otherwise it ends up in the core IR namespace. In the SPIR-V backend we want the namespace to be tint::spirv::type
so we declare enums as:
@ns("spirv::type") enum arrayed { NonArrayed Arrayed }
The @ns("spirv::type")
will put the enum into the correct namespace when emitted.