<!-- Source: https://medius.k4tech.net/native/commands/transform -->
# Transform

_Swap or remap a field on the wire_

[`TRANSFORM`](/native/commands/transform.md#transform) moves a field the clone's descriptor declares into another one, clamped to the destination's declared range, so the clone still emits only values the real device could. It carries no [imperfect-clone opt-in](/native/commands/option.md#imperfect), unlike the rewrite/raw/patch layer.

```
  parsed report --> LOCK --> [ TRANSFORM ] --> render --> inject --> emit
                                   |
                                   +-- swap    two axes, read both then write both
                                   +-- remap   source -> destination, source cleared
                                         |
                                         +-- button -> key / media, held on the
                                             destination's own interface
```

| op | Name | Effect |
| --- | --- | --- |
| `0` | `REMAP` | move the source field into the destination and clear the source |
| `1` | `SWAP` | exchange two axes: read both, then write both |

To weigh a field, or reverse it, use [`LOCK`](/native/commands/lock.md#scale), whose percent is signed. An `op` above `1` is refused.

A field is a `(class, id)` in the same space [`INJECT`](/native/commands/inject.md#inject) and [`LOCK`](/native/commands/lock.md) use: `0` button, `1` key, `2` media, `3` axis (id `0`\=X, `1`\=Y, `2`\=wheel, `3`\=pan).

[`QUERY(TRANSFORMS)`](/native/commands/requests.md#transforms) reads the installed table back.

## TRANSFORM

_Install, overwrite, or remove one entry_

An entry is keyed by its `(source, dest)`; setting one whose key exists overwrites its op. [Opcode](/native/frame.md#opcodes) `0x1E`.

```text
TRANSFORM 0x1E · payload 8 bytes
```

_Fire-and-forget_

#### PAYLOAD

| Offset | Field | Type | Notes |
| --- | --- | --- | --- |
| 0 | `op` | `u8` | the operation, as the [table above](/native/commands/transform.md) |
| 1 | `sclass` | `u8` | source class: `0` button, `1` key, `2` media, `3` axis |
| 2 | `sid` | `u16` | source id within the class, little-endian |
| 4 | `dclass` | `u8` | destination class |
| 5 | `did` | `u16` | destination id, little-endian |
| 7 | `state` | `u8` | `1` set (add or overwrite), `0` remove the keyed entry |

#### WITH A SCALE

The [weigh](/native/commands/lock.md#scale) runs first and the transform moves what it kept. The rounding remainder is banked once, per axis and sign, by the lock.

```
  LOCK(X, both, -50)      keep half of X, reversed
  TRANSFORM(swap, X, Y)   and put what is left on Y

  physical X = +10  ->  weighed -5  ->  emitted on Y
```

#### REFUSALS

| Refused when | Why |
| --- | --- |
| `op` is above `1` | remap and swap are the whole set |
| the op does not admit that [class pair](/native/commands/transform.md#pairs) | each op names the shapes it can read and write |
| the source and the destination are the same field | a move needs two; to weigh a field in place, use the [lock](/native/commands/lock.md#scale) |
| a field this clone does not declare | the box will not store an address it cannot reach; re-send the entry after a re-clone |
| the table already holds 32 entries | nothing is evicted; the readback's full flag says an entry was turned away |

#### EFFECT

An entry takes effect on the next report the device sends. The box walks the table in the order entries were installed, so a swap installed after a remap exchanges what the remap wrote.

`TRANSFORM` has no reply, so a refused entry shows up as its absence from [`RESP(TRANSFORMS)`](/native/commands/requests.md#transforms). That reply also carries the full flag, and the `TRANSFORM_ON` [health](/native/commands/requests.md#health) bit follows the table.

#### EXAMPLE

Swap X and Y: `op = 1`, source `(axis 3, id 0)`, dest `(axis 3, id 1)`, `state = 1`:

```
+--------+--------+--------+--------+--------+--------+
| A5     | 1E     | 00     | 08 00  | 01     | 03     |
+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | sclass |
+--------+--------+--------+--------+--------+--------+

+--------+--------+--------+--------+--------+
| 00 00  | 03     | 01 00  | 01     | lo hi  |
+--------+--------+--------+--------+--------+
| sid    | dclass | did    | state  | CRC16  |
+--------+--------+--------+--------+--------+
```

The wheel drives vertical motion: `op = 0` (remap), source `(axis 3, id 2)`, dest `(axis 3, id 1)`:

```
+--------+--------+--------+--------+--------+--------+
| A5     | 1E     | 01     | 08 00  | 00     | 03     |
+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | sclass |
+--------+--------+--------+--------+--------+--------+

+--------+--------+--------+--------+--------+
| 02 00  | 03     | 01 00  | 01     | lo hi  |
+--------+--------+--------+--------+--------+
| sid    | dclass | did    | state  | CRC16  |
+--------+--------+--------+--------+--------+
```

Library bindings: [`transform`](/library/transform.md#transform), [`transform_swap`](/library/transform.md#helpers), and [`transform_remap`](/library/transform.md#helpers).

## Field pairs

_Which source and destination each op admits_

Each op reads and writes a fixed set of shapes. Anything else is refused.

```
  swap    axis a    <--------->  axis b      two axes
  remap   axis a    ---------->  axis b      one report, source zeroed
          button i  ---------->  button j    one report, source bit cleared
          button i  ---------->  key         the keyboard collection
          button i  ---------->  media       the consumer collection

  every pair is two different fields
```

| Name | What the box does |
| --- | --- |
| `swap` | Reads both axes, then writes both. Two remaps would read the second after the first had overwritten it and leave the pair equal. |
| `remap` | Adds the source onto the destination's own value, then zeroes the source. A button destination is OR'd the press instead, and the source bit is cleared. |

#### WHAT A REMAP LEAVES

An axis remap adds, so a destination that was already moving keeps its own motion and picks up the source's on top. Only the source is zeroed. The sum is clamped to the destination's declared range like any other result.

## Button to key or media

_The one remap that crosses collections_

A button source can drive a keyboard or Consumer destination. The key or media usage is emitted through that collection's own interface, exactly as [`INJECT`](/native/commands/inject.md#key) emits one.

#### HELD, NOT LATCHED

```
  physical button    ____----------________-----____
  key on the clone   ____----------________-----____

  driven from the button mask the device just reported,
  re-read every report, with no edge latch to strand
```

Because it follows the level, a configuration switch, a missed release, or a destination that was briefly unbound all resolve on the next report instead of leaving a key held with nothing pressed.

#### EVERY MOUSE COLLECTION

A device may split its buttons across two mouse collections. The destination is held while any collection the box reports on has that button down, and a collection that does not declare the id holds nothing for it.

#### INERT WHEN UNBOUND

An entry naming a destination collection the box has not bound is refused outright. One whose collection goes away with a configuration switch turns inert: it does nothing and does not clear its source, so the button keeps reaching the game PC until that collection binds again.

#### EXAMPLE

Side button 3 drives the `a` key: `op = 0` (remap), source `(button 0, id 3)`, dest `(key 1, id 0x04)`:

```
+--------+--------+--------+--------+--------+--------+
| A5     | 1E     | 02     | 08 00  | 00     | 00     |
+--------+--------+--------+--------+--------+--------+
| SOF    | TYPE   | SEQ    | LEN    | op     | sclass |
+--------+--------+--------+--------+--------+--------+

+--------+--------+--------+--------+--------+
| 03 00  | 01     | 04 00  | 01     | lo hi  |
+--------+--------+--------+--------+--------+
| sid    | dclass | did    | state  | CRC16  |
+--------+--------+--------+--------+--------+
```

The keycode is a [HID keyboard usage](/native/commands/usage.md#keycodes); a media destination takes a 16-bit [Consumer usage](/native/commands/usage.md#consumer).

## Where the pass sits

_Against locks, rendering, and injection_

The weigh comes first and the field pass moves what it left, so a transform carries a weighed value and everything downstream reads the field where the table put it.

```
  physical report
       |
       +-- 1  LOCK        weighs each field on its own sign and bearing
       |
       +-- 2  TRANSFORM   moves the weighed fields, in table order
       |
       +-- 3  render      the model is handed the transformed, weighed delta
       |
       +-- 4  inject      drains onto the field a remap just cleared
       |
       v
  emitted report
```

| Stage | Reads | Acts on |
| --- | --- | --- |
| [`LOCK`](/native/commands/lock.md) | the physical field | a lock bites on the sign the device reported, not on where the value ends up, so a swap never moves a lock with it |
| [rendering](/native/commands/option.md#render) | the transformed, weighed cursor delta | the model is fed the same numbers the wire would have carried, so a scale changes what it renders rather than what it corrects |
| [injection](/native/injection.md) | nothing the table wrote | injected motion drains into the axis after the pass; a remap that zeroed that axis does not take it with it |

#### WHAT CATCH SEES

The input classes of [`CATCH`](/native/commands/catch.md#catch) tap the physical report before either pass, so they still report the value the device sent. Its `EMIT` class is the mirror and carries what the clone actually put on the wire.

## Lifecycle

_A transform is PC-owned session state_

#### CLEARS ON

```
remove      a TRANSFORM with state = 0 and the entry's (source, dest)
clear       state 0 with both classes 0xFF and both ids 0xFFFF, a blanket
silence     ~1 s with no control-PC frame
RESET       a RESET command
link loss   the inter-chip link drops
detach      the real device goes away
re-clone    the box binds a device again
```

Any valid frame resets the silence timer, so a keepalive holds the table open. The host library re-asserts the whole table after a device-side blip and across a control-link reconnect, exactly as it does a [`LOCK`](/native/commands/lock.md).

#### WHAT A CLEAR RELEASES

The [cross-class holds](/native/commands/transform.md#cross) go before the entries do, so no key or media usage is left down once the entry that drove it is gone. Removing one entry releases only what that entry was holding.

[`RESET`](/native/commands/admin.md#reset), a detach and a link drop take the table through the same release, alongside [injection and locks](/native/injection.md#safety).
