| # 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. |
| |