mv.swizzle

Links

Swizzle is a type of permute shorthand allowing arbitrary selection of elements from vec2/3/4 creating a new vec2/3/4. Their value lies in the high occurrence of Swizzle in 3D Shader Binaries (over 10% of all instructions). Swizzle is usually done on a per-vec-operand basis in 3D GPU ISAs, making for extremely long instructions (64 bits or greater), however it is not practical to add two or more sets of 12-bit prefixes into a single instruction. A compromise is to provide a Swizzle "Move": one such move is then required for each operand used in a subsequent instruction. The encoding for Swizzle Move embeds static predication into the swizzle as well as constants 1/1.0 and 0/0.0, and if Saturation is enabled maximum arithmetic constants may be placed into the destination as well.

An extremely important aspect of 3D GPU workloads is that the source and destination subvector lengths may be different. A vector of contiguous array of vec3 (XYZ) may only have 2 elements (ZY) swizzle-copied to a contiguous array of vec2. A contiguous array of vec2 sources may have multiple of each vec2 elements (XY) copied to a contiguous vec4 array (YYXX or XYXX). For this reason, when Vectorized Swizzle Moves support independent subvector lengths for both source and destination.

Although conceptually similar to vpermd and vpermdi of Packed SIMD VSX, Swizzle Moves come in immediate-only form with only up to four selectors, where vpermd refers to individual bytes and may not copy constants to the destination. 3D Shader programs commonly use the letters "XYZW" when referring to the four swizzle indices, and also often use the letters "RGBA" if referring to pixel data. These designations are also part of both the OpenGL(TM) and Vulkan(TM) specifications.

As a standalone Scalar operation this instruction is valuable if Prefixed with SVP64Single (providing Predication). Combined with cmpi it synthesises Compare-and-Swap. It is also more flexible than xxpermdi.

Format

0.5 6.10 11.15 16.27 28.31 name Form
PO RTp RAp imm 0011 mv.swiz DQ-Form
PO RTp RAp imm 1011 fmv.swiz DQ-Form

this gives a 12 bit immediate across bits 16 to 27. Each swizzle mnemonic (XYZW), commonly known from 3D GPU programming, has an associated index. 3 bits of the immediate are allocated to each:

imm 0.2 3.5 6.8 9.11
swizzle X Y Z W
pixel R G B A
index 0 1 2 3

The options for each Swizzle are:

  • 0b000 to indicate "skip". this is equivalent to predicate masking
  • 0b001 subvector length end marker (length=4 if not present)
  • 0b010 to indicate "constant 0"
  • 0b011 to indicate "constant 1" (or 1.0)
  • 0b1NN index 0 thru 3 to copy from subelement in pos XYZW

In very simplistic terms the relationship between swizzle indices (NN, above), source, and destination is:

dest[i] = src[swiz[i]]

Note that 8 options are needed (not 6) because option 0b001 encodes the subvector length, and option 0b000 allows static predicate masking (skipping) to be encoded within the swizzle immediate. For example it allows "W.Y." to specify: "copy W to position X, and Y to position Z, leave the other two positions Y and W unaltered"

0    1    2    3
X    Y    Z    W  source
     |         |     
     +----+    |
     .    |    |
+--------------+
|    .    |    .
W    .    Y    .  swizzle
|    .    |    .
|    Y    |    W  Y,W unmodified
|    .    |    .
W    Y    Y    W  dest

As a Scalar instruction

Given that XYZW Swizzle can select simultaneously between one and four register operands, a full version of this instruction would be an eye-popping 8 64-bit operands: 4-in, 4-out. As part of a Scalar ISA this not practical. A compromise is to cut the registers required by half, placing it on-par with lq, stq and Indexed Load-with-update instructions. When part of the Scalar Power ISA (not SVP64 Vectorized) mv.swiz and fmv.swiz operate on four 32-bit quantities, reducing this instruction to a feasible 2-in, 2-out pairs of 64-bit registers:

swizzle name source dest half
X RA RT lo-half
Y RA RT hi-half
Z RA+1 RT+1 lo-half
W RA+1 RT+1 hi-half

When RA=RT (in-place swizzle) any portion of RT not covered by the Swizzle is unmodified. For example a Swizzle of "..XY" will copy the contents RA+1 into RT but leave RT+1 unmodified.

When RA!=RT any part of RT or RT+1 not set as a destination by the Swizzle will be set to zero. A Swizzle of "..XY" would copy the contents RA+1 into RT, but set RT+1 to zero.

Also, making life easier, RT and RA are only permitted to be even (no overlapping can occur). This makes RT (and RA) a "pair" exactly as in lq and stq. Scalar Swizzle instructions must be atomically indivisible: an Exception or Interrupt may not occur during the Moves.

Note that unlike the Vectorized variant, when RT=RA the Scalar variant must buffer (read) both 64-bit RA registers before writing to the RT pair (in an Out-of-Order Micro-architecture, both of the register pair must be "in-flight"). This ensures that register file corruption does not occur.

SVP64 Vectorized

Vectorized Swizzle may be considered to contain an extended static predicate mask for subvectors (SUBVL=2/3/4). Due to the skipping caused by the static predication capability, the destination subvector length can be different from the source subvector length, and consequently the destination subvector length is encoded into the Swizzle.

When Vectorized, given the use-case is for a High-performance GPU, the fundamental assumption is that Micro-coding or other technique will be deployed in hardware to issue multiple Scalar MV operations and full parallel crossbars, which would be impractical in a smaller Scalar-only Micro-architecture. Therefore the restriction imposed on the Scalar mv.swiz to 32-bit quantities as the default is lifted on sv.mv.swiz.

Additionally, in order to make life easier for implementers, some of whom may wish, especially for Embedded GPUs, to use multi-cycle Micro-coding, the usual strict Element-level Program Order is relaxed. An overlap between all and any Vectorized sources and destination Elements for the entirety of the Vector Loop 0..VL-1 is UNDEFINED behaviour.

This in turn implies that Traps and Exceptions are, as usual, permitted in between element-level moves, because due to there being no overlap there is no risk of destroying a source with an overwrite. This is unlike the Scalar variant which, when RT=RA, must buffer both halves of the RT pair.

Determining the source and destination subvector lengths is tricky. Swizzle Pseudocode:

    swiz[0] = imm[0:3]   # X
    swiz[1] = imm[3:6]   # Y
    swiz[2] = imm[6:9]   # Z
    swiz[3] = imm[9:12]  # W
    # determine implied subvector length from Swizzle
    dst_subvl = 4
    for i in range(4):
        if swiz[i] == 0b001:
            dst_subvl = i+1
            break

What is going on here is that the option is provided to have different source and destination subvector lengths, by exploiting redundancy in the Swizzle Immediate. With the Swizzles marking what goes into each destination position, the marker "0b001" may be used to indicate the end. If no marker is present then the destination subvector length may be assumed to be 4. SUBVL is considered to be the "source" subvector length.

Pseudocode exploiting python "yield" for clarity: element-width overrides, Saturation and Predication also left out, for clarity:

    def index_src():
        for i in range(VL):
            for j in range(SUBVL):
                if swiz[j] == 0b000: # skip
                    continue
                if swiz[j] == 0b001: # end
                    break
                if swiz[j] in [0b010, 0b011]:
                    yield (i*SUBVL, CONSTANT)
                else:
                    yield (i*SUBVL, swiz[j]-3)

    def index_dest():
        for i in range(VL):
            for j in range(dst_subvl):
                if swiz[j] == 0b000: # skip
                    continue
                yield i*dst_subvl+j

    # walk through both source and dest indices simultaneously
    for (src_idx, offs), dst_idx in zip(index_src(), index_dst()):
        if offs == CONSTANT:
             set(RT+dst_idx, CONSTANT)
        else
             move_operation(RT+dst_idx, RA+src_idx+offs)

Vertical-First Mode

It is important to appreciate that only the main loop VL is Vertical-First: the SUBVL loop is not. This makes sense from the perspective that the Swizzle Move is a group of moves, but is still a single instruction that happens to take vec2/3/4 as operands. Vertical-First only performing one of the sub-elements at a time rather than operating on the entire vec2/3/4 together would violate that expectation. The exceptions to this, explained later, are when Pack/Unpack is enabled.

Effect of Saturation on Vectorized Swizzle

A useful convenience for pixel data is to be able to insert values 0x7f or 0xff as magic constants for arbitrary R,G,B or A. Therefore, when Saturation is enabled and a Swizzle=0b011 (Constant 1) is requested, the maximum permitted Saturated value is inserted rather than Constant 1. sv.mv.swiz/sats/vec2/ew=8 RT.v, RA.v, Y1 would insert the 2nd subelement (Y) into the first destination subelement and the signed-maximum constant 0x7f into the second. A Constant 0 Swizzle on the other hand still inserts zero because there is no encoding space to select between -1, 0 and 1, and 0 and max values are more useful.

Pack/Unpack Mode:

It is possible to apply Pack and Unpack to Vectorized swizzle moves. The interaction requires specific explanation because it involves the separate SUBVLs (with destination SUBVL being separate). Key to understanding is that the source and destination SUBVL be "outer" loops instead of inner loops, exactly as in remap Matrix mode, under the control of SVSTATE.PACK and SVSTATE.UNPACK.

Illustrating a "normal" SVP64 operation with SUBVL!=1 (assuming no elwidth overrides):

    def index():
        for i in range(VL):
            for j in range(SUBVL):
                yield i*SUBVL+j

    for idx in index():
        operation_on(RA+idx)

For a separate source/dest SUBVL (again, no elwidth overrides):

    # yield an outer-SUBVL or inner VL loop with SUBVL
    def index_dest(outer):
        if outer:
            for j in range(dst_subvl):
                for i in range(VL):
                    yield j*VL+i
        else:
            for i in range(VL):
                for j in range(dst_subvl):
                    yield i*dst_subvl+j

    # yield an outer-SUBVL or inner VL loop with SUBVL
    def index_src(outer):
        if outer:
            for j in range(SUBVL):
                for i in range(VL):
                    yield j*VL+i
        else:
            for i in range(VL):
                for j in range(SUBVL):
                    yield i*SUBVL+j

"yield" from python is used here for simplicity and clarity. The two Finite State Machines for the generation of the source and destination element offsets progress incrementally in lock-step.

Just as in mv.vec, when PACK_en is set it is the source that swaps to Outer-subvector loops, and when UNPACK_en is set it is the destination that swaps its loop-order. Setting both PACK_en and UNPACK_en is neither prohibited nor UNDEFINED because the behaviour is fully deterministic.

However, in Vertical-First Mode, when both are enabled, with both source and destination being outer loops a single step of srstep and dststep is performed. Contrast this when one of PACK_en is set, it is the destination that is an inner subvector loop, and therefore Vertical-First runs through the entire dst_subvl group. Likewise when UNPACK_en is set it is the source subvector that is run through as a group.

if VERTICAL_FIRST:
    # must run through SUBVL or dst_subvl elements, to keep
    # the subvector "together".  weirdness occurs due to
    # PACK_en/UNPACK_en
    num_runs = SUBVL # 1-4
    if PACK_en:
        num_runs = dst_subvl # destination still an inner loop
    if PACK_en and UNPACK_en:
        num_runs = 1 # both are outer loops
    for substep in num_runs:
        (src_idx, offs) = yield from index_src(PACK_en)
        dst_idx = yield from index_dst(UNPACK_en)
        move_operation(RT+dst_idx, RA+src_idx+offs)