blob: bbc8070f54cfe0e9649fc413f5066ebf96f67b5a [file] [log] [blame] [view]
# 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](intrinsic_defintion_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.