IR Extensions

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.

Transforms

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.

Intrinsics

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.

Instructions

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}))

Types

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.

Enums

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.