Shader Instruction Set

Shader Instruction Set

Overview #

A compiled shader binary is comprised of two parts : the main instruction sequence and the operand descriptor table. These are both sent to the GPU around the same time but using separate GPU Commands. Instructions (such as format 1 instruction) may reference operand descriptors. When such is the case, the operand descriptor ID is the offset, in words, of the descriptor within the table. Both instructions and descriptors are coded in little endian. Basic implementations of the following specification can be found at πŸ”— 1 and πŸ”— 2. The instruction set seems to have been heavily inspired by Microsoft’s vs_3_0 πŸ”— 3 and the Direct3D shader code πŸ”— 4. Please note that this page is being written as the instruction set is reverse engineered; as such it may very well contain mistakes.

Debug information found in the code.bin of “Ironfall: Invasion” suggests that there may not be more than 512 instructions and 128 operand descriptors in a shader.

Nomenclature #

  • opcode names with I appended to them are the same as their non-I version, except they use the inverted instruction format, giving 7 bits to SRC2 (and access to constant registers) and 5 bits to SRC1
  • opcode names with U appended to them are the same as their non-U version, except they are executed conditionally based on the value of a constant boolean register.
  • opcode names with C appended to them are the same as their non-C version, except they are executed conditionally based on a logical expression specified in the instruction.

Instruction formats #

Format 1 : (used for register operations)

OffsetSize (bits)Description
0x00x7Operand descriptor ID (DESC)
0x70x5Source 2 register (SRC2)
0xC0x7Source 1 register (SRC1)
0x130x2Address register index for SRC1 (IDX_1)
0x150x5Destination register (DST)
0x1A0x6Opcode

Format 1i : (used for register operations)

OffsetSize (bits)Description
0x00x7Operand descriptor ID (DESC)
0x70x7Source 2 register (SRC2)
0xE0x5Source 1 register (SRC1)
0x130x2Address register index for SRC2 (IDX_2)
0x150x5Destination register (DST)
0x1A0x6Opcode

Format 1u : (used for unary register operations)

OffsetSize (bits)Description
0x00x7Operand descriptor ID (DESC)
0xC0x7Source 1 register (SRC1)
0x130x2Address register index for SRC1 (IDX_1)
0x150x5Destination register (DST)
0x1A0x6Opcode

Format 1c : (used for comparison operations)

OffsetSize (bits)Description
0x00x7Operand descriptor ID (DESC)
0x70x5Source 2 register (SRC2)
0xC0x7Source 1 register (SRC1)
0x130x2Address register index for SRC1 (IDX_1)
0x150x3Comparison operator for Y (CMPY)
0x180x3Comparison operator for X (CMPX)
0x1B0x5Opcode

Format 2 : (used for flow control instructions)

OffsetSize (bits)Description
0x00x8Number of instructions (NUM)
0xA0xCDestination offset (in words) (DST)
0x160x2Condition boolean operator (CONDOP)
0x180x1Y reference bit (REFY)
0x190x1X reference bit (REFX)
0x1A0x6Opcode

Format 3 : (used for constant-based conditional flow control instructions)

OffsetSize (bits)Description
0x00x8Number of instructions ? (NUM)
0xA0xCDestination offset (in words) (DST)
0x160x4Constant ID (BOOL/INT)
0x1A0x6Opcode

Format 4 : (used for SETEMIT)

OffsetSize (bits)Description
0x160x1Winding flag (FLAG_WINDING)
0x170x1Primitive emit flag (FLAG_PRIMEMIT)
0x180x2Vertex ID (VTXID)
0x1A0x6Opcode

Format 5 : (used for MAD)

OffsetSize (bits)Description
0x00x5Operand descriptor ID (DESC)
0x50x5Source 3 register (SRC3)
0xA0x7Source 2 register (SRC2)
0x110x5Source 1 register (SRC1)
0x160x2Address register index for SRC2 (IDX_2)
0x180x5Destination register (DST)
0x1D0x3Opcode

Format 5i : (used for MADI)

OffsetSize (bits)Description
0x00x5Operand descriptor ID (DESC)
0x50x7Source 3 register (SRC3)
0xC0x5Source 2 register (SRC2)
0x110x5Source 1 register (SRC1)
0x160x2Address register index for SRC3 (IDX_3)
0x180x5Destination register (DST)
0x1D0x3Opcode

Instructions #

Unless noted otherwise, SRC1 and SRC2 refer to their respectively indexed float[4] registers (after swizzling). Similarly, DST refers to its indexed register modulo destination component masking, i.e. an expression like DST=SRC1 might actually just set DST.y to SRC1.y.

OpcodeFormatNameDescription
0x001ADDAdds two vectors component by component; DST[i] = SRC1[i]+SRC2[i] for all i
0x011DP3Computes dot product on 3-component vectors; DST = SRC1.SRC2
0x021DP4Computes dot product on 4-component vectors; DST = SRC1.SRC2
0x031DPHComputes dot product on a 3-component vector with 1.0 appended to it and a 4-component vector; DST = SRC1.SRC2 (with SRC1 homogenous)
0x041DSTEquivalent to Microsoft’s πŸ”— dst instruction: DST = {1, SRC1[1]*SRC2[1], SRC1[2], SRC2[3]}
0x051uEX2Computes SRC1’s first component exponent with base 2; DST[i] = EXP2(SRC1[0]) for all i
0x061uLG2Computes SRC1’s first component logarithm with base 2; DST[i] = LOG2(SRC1[0]) for all i
0x071uLITPPartial lighting computation, may be used in conjunction with EX2, LG2, etc to compute the vertex lighting coefficients. See the πŸ”— Microsoft and πŸ”— ARB docs for more information on how to implement the full lit function; DST = {max(src.x, 0), max(min(src.y, 127.9961), -127.9961), 0, max(src.w, 0)} and it sets the cmp.x and cmp.y flags based on if the respective src.x and src.w components are >= 0.
0x081MULMultiplies two vectors component by component; DST[i] = SRC1[i].SRC2[i] for all i
0x091SGESets output if SRC1 is greater than or equal to SRC2; DST[i] = (SRC1[i] >= SRC2[i]) ? 1.0 : 0.0 for all i
0x0A1SLTSets output if SRC1 is strictly less than SRC2; DST[i] = (SRC1[i] < SRC2[i]) ? 1.0 : 0.0 for all i
0x0B1uFLRComputes SRC1’s floor component by component; DST[i] = FLOOR(SRC1[i]) for all i
0x0C1MAXTakes the max of two vectors, component by component; DST[i] = MAX(SRC1[i], SRC2[i]) for all i
0x0D1MINTakes the min of two vectors, component by component; DST[i] = MIN(SRC1[i], SRC2[i]) for all i
0x0E1uRCPComputes the reciprocal of the vector’s first component; DST[i] = 1/SRC1[0] for all i
0x0F1uRSQComputes the reciprocal of the square root of the vector’s first component; DST[i] = 1/sqrt(SRC1[0]) for all i
0x10?????
0x11?????
0x121uMOVAMove to address register; Casts the float value given by SRC1 to an integer (truncating the fractional part) and assigns the result to (a0.x, a0.y, _, _), respecting the destination component mask.
0x131uMOVMoves value from one register to another; DST = SRC1.
0x14?????
0x15?????
0x16?????
0x17?????
0x181iDPHIComputes dot product on a 3-component vector with 1.0 appended to it and a 4-component vector; DST = SRC1.SRC2 (with SRC1 homogenous)
0x191iDSTIDST with sources swapped.
0x1A1iSGEISets output if SRC1 is greater than or equal to SRC2; DST[i] = (SRC1[i] >= SRC2[i]) ? 1.0 : 0.0 for all i
0x1B1iSLTISets output if SRC1 is strictly less than SRC2; DST[i] = (SRC1[i] < SRC2[i]) ? 1.0 : 0.0 for all i
0x1C?????
0x1D?????
0x1E?????
0x1F?????
0x200BREAKBreaks out of LOOP block; do not use while in nested IF/CALL block inside LOOP block.
0x210NOPDoes literally nothing.
0x220ENDSignals the shader unit that processing for this vertex/primitive is done.
0x232BREAKCIf condition (see below for details) is true, then breaks out of LOOP block.
0x242CALLJumps to DST and executes instructions until it reaches DST+NUM instructions
0x252CALLCIf condition (see below for details) is true, then jumps to DST and executes instructions until it reaches DST+NUM instructions, else does nothing.
0x263CALLUJumps to DST and executes instructions until it reaches DST+NUM instructions if BOOL is true
0x273IFUIf condition BOOL is true, then executes instructions until DST, then jumps to DST+NUM; else, jumps to DST.
0x282IFCIf condition (see below for details) is true, then executes instructions until DST, then jumps to DST+NUM; else, jumps to DST
0x293LOOPLoops over the code between itself and DST (inclusive), performing INT.x+1 iterations in total. First, aL is initialized to INT.y. After each iteration, aL is incremented by INT.z.
0x2A0 (no param)EMIT(geometry shader only) Emits a vertex (and primitive if FLAG_PRIMEMIT was set in the corresponding SETEMIT). SETEMIT must be called before this.
0x2B4SETEMIT(geometry shader only) Sets VTXID, FLAG_WINDING and FLAG_PRIMEMIT for the next EMIT instruction. VTXID is the ID of the vertex about to be emitted within the primitive, while FLAG_PRIMEMIT is zero if we are just emitting a single vertex and non-zero if are emitting a vertex and primitive simultaneously. FLAG_WINDING controls the output primitive’s winding. Note that the output vertex buffer (which holds 4 vertices) is not cleared when the primitive is emitted, meaning that vertices from the previous primitive can be reused for the current one. (this is still a working hypothesis and unconfirmed)
0x2C2JMPCIf condition (see below for details) is true, then jumps to DST, else does nothing.
0x2D3JMPUIf condition BOOL is true, then jumps to DST, else does nothing. Having bit 0 of NUM = 1 will invert the test, jumping if BOOL is false instead.
0x2E-0x2F1cCMPSets booleans cmp.x and cmp.y based on the operand’s x and y components and the CMPX and CMPY comparison operators respectively. See below for details about operators. It’s unknown whether CMP respects the destination component mask or not.
0x30-0x375iMADIMultiplies two vectors and adds a third one component by component; DST[i] = SRC3[i] + SRC2[i].SRC1[i] for all i; this is not an FMA, the intermediate result is rounded
0x38-0x3F5MADMultiplies two vectors and adds a third one component by component; DST[i] = SRC3[i] + SRC2[i].SRC1[i] for all i; this is not an FMA, the intermediate result is rounded

Operand descriptors #

Sizes below are in bits, not bytes.

OffsetSizeDescription
0x00x4Destination component mask. Bit 3 = x, 2 = y, 1 = z, 0 = w.
0x40x1Source 1 negation bit
0x50x8Source 1 component selector
0xD0x1Source 2 negation bit
0xE0x8Source 2 component selector
0x160x1Source 3 negation bit
0x170x8Source 3 component selector

Component selector :

OffsetSizeDescription
0x00x2Component 3 value
0x20x2Component 2 value
0x40x2Component 1 value
0x60x2Component 0 value
ValueComponent
0x0x
0x1y
0x2z
0x3w

The component selector enables swizzling. For example, component selector 0x1B is equivalent to .xyzw, while 0x55 is equivalent to .yyyy.

Depending on the current shader opcode, source components are disabled implicitly by setting the destination component mask. For example, ADD o0.xy, r0.xyzw, r1.xyzw will not make use of r0’s or r1’s z/w components, while DP4 o0.xy, r0.xyzw, r1.xyzw will use all input components regardless of the used destination component mask.

Relative addressing #

IDX raw valueRegister name
0x0None
0x1a0.x
0x2a0.y
0x3aL

There are 3 address registers: a0.x, a0.y and aL (loop counter). For format 1 instructions, when IDX != 0, the value of the corresponding address register is added to SRC1’s value. For example, if IDX = 2, a0.y = 3 and SRC1 = c8, then instead SRC1+a0.y = c11 will be used for the instruction. It is only possible to use address registers on constant registers, attempting to use them on input attribute or temporary registers results in the address register being ignored (i.e. read as zero).

a0.x and a0.y are set manually through the MOVA instruction by rounding a float value to integer precision. Hence, they may take negative values. The way out-of-bounds values behave when reading uniforms is as follows:

  • If the offset is out of byte bounds (less than -128 or greater than 127), the offset is not applied (treated as 0).
  • The offset is added to the constant register index and masked by 0x7F.
  • If the resulting index is greater than 95, the result is (1, 1, 1, 1).
  • Otherwise, the result is the value at the indexed constant register.

aL can only be set indirectly by the LOOP instruction. It is still accessible and valid after exiting a LOOP block, though.

Comparison operator #

CMPX/CMPY raw valueOperator nameExpression
0x0EQsrc1 == src2
0x1NEsrc1 != src2
0x2LTsrc1 < src2
0x3LEsrc1 <= src2
0x4GTsrc1 > src2
0x5GEsrc1 >= src2
0x6??true ?
0x7??true ?

6 and 7 seem to always return true.

Conditions #

A number of format 2 instructions are executed conditionally. These
conditions are based on two boolean registers which can be set with CMP
cmp.x and cmp.y.

Conditional instructions include 3 parameters : CONDOP, REFX and REFY. REFX and REFY are reference values which are tested for equality against cmp.x and cmp.y, respectively. CONDOP describes how the final truth value is constructed from the results of the two tests. There are four conditional expression formats :

CONDOP raw valueExpressionDescription
0x0cmp.x == REFXcmp.y == REFY
0x1cmp.x == REFX && cmp.y == REFYAND
0x2cmp.x == REFXX
0x3cmp.y == REFYY

Registers #

NameFormatTypeAccessWritten byDescription
v0-v15vectorfloatRead onlyApplication/Vertex-streamInput registers.
o0-o15vectorfloatWrite onlyVertex shaderOutput registers.
r0-r15vectorfloatRead/WriteVertex shaderTemporary registers.
c0-c95vectorfloatRead onlyApplication/Vertex-streamFloating-point Constant registers.
i0-i3vectorintegerRead onlyApplicationInteger Constant registers. (special purpose)
b0-b15scalarbooleanRead onlyApplicationBoolean Constant registers. (special purpose)
a0.x & a0.yscalarintegerUse/WriteVertex shaderAddress registers.
aLscalarintegerUseVertex shaderLoop count register.

Input attribute registers store the per-vertex data given by the CPU and hence are read-only.

Output registers hold the data to be passed to the later GPU stages and are write-only. Each of the output register is assigned a semantic by setting the corresponding GPU_Internal_Registers. Output registers o7-o15 are only available in vertex shaders. Keep in mind that writing to the same output register/component more than once appears appears to cause problems (e.g. GPU hangs).

Temporary registers can be used for intermediate calculations and can be both read and written.

Constant registers hold data uploaded by the application which remain constant throughout all processed vertices. There are 96 float[4] constant registers (c0-c95), eight boolean constant registers (b0-b7), and four int[4] constant registers (i0-i3). Many shader instructions which take float arguments can only provide the full 7 bits for one SRC operand. All other source operands can only be used to refer to input attributes or temporary registers and cannot be passed Floating-point Constant registers.

Address registers and the Loop count register can be used to to provide relative addressing for the designated SRC operand. For more information, see the section on relative addressing.

DST mapping :

DST raw valueRegister nameDescription
0x0-0xFo0-o15Output registers.
0x10-0x1Fr0-r15Temporary registers.

SRC mapping :

SRC raw valueRegister nameDescription
0x0-0xFv0-v15Input attribute registers.
0x10-0x1Fr0-r15Temporary registers.
0x20-0x7Fc0-c95Constant registers.

Floating-Point Behavior #

The PICA200 is not IEEE-compliant. It has positive and negative infinities and NaN, but does not seem to have negative 0. Input and output subnormals are flushed to +0. The internal floating point format seems to be the same as used in shader binaries: 1 sign bit, 7 exponent bits, 16 (explicit) mantissa bits. Several instructions also have behavior that differs from the IEEE functions. Here are the results from some tests done on hardware (s = largest subnormal, n = smallest positive normal):

ComputationResultNotes
inf * 00Including inside MUL, MAD, DP4, etc.
NaN * 0NaN
+inf - +infNaNIndicates +inf is real inf, not FLT_MAX
rsq(rcp(-inf))+infIndicates that there isn’t -0.0.
rcp(-0)+infno -0 so differs from IEEE where rcp(-0) = -inf
rcp(0)+inf
rcp(+inf)0
rcp(NaN)NaN
rsq(-0)+infno -0 so differs from IEEE where rsq(-0) = -inf
rsq(-2)NaN
rsq(+inf)0
rsq(-inf)NaN
rsq(NaN)NaN
max(0, +inf)+inf
max(0, -inf)-inf
max(0, NaN)NaNmax violates IEEE but match GLSL spec
max(NaN, 0)0
max(-inf, +inf)+inf
min(0, +inf)0
min(0, -inf)-inf
min(0, NaN)NaNmin violates IEEE but match GLSL spec
min(NaN, 0)0
min(-inf, +inf)-inf
cmp(s, 0)falsecmp does not flush input subnormals
max(s, 0)smax does not flush input or output subnormals
mul(s, 2)0input subnormals are flushed in arithmetic instructions
mul(n, 0.5)0output subnormals are flushed in arithmetic instructions

1.0 can be multiplied 63 times by 0.5 until the result compares equal zero. This is consistent with a 7-bit exponent and output subnormal flushing.

Control Flow #

Control flow is implemented using four independent stacks:

  • 4-deep CALL stack
  • 8-deep IF stack
  • 4-deep LOOP stack

All stacks are initially empty. After every instruction but before JMP takes effect, the PC is incremented and a copy is sent to each stack. Each stack is checked against its copy of the PC. If an entry is popped from the stack, the copied PC is updated and used for the next check of this stack, although the IF/LOOP stacks can each only pop one entry per instruction, whereas the CALL stack is checked again until it doesn’t match or the stack is empty. The updated PC copy with the highest priority wins: LOOP (highest), IF, CALL, JMP, original PC (lowest).

Special cases:

  • JMP overwrites the PC *after* the stacks checks (and only if no stack was popped).
  • Executing a BREAK on an empty LOOP stack hangs the GPU.
  • A stack overflow discards the oldest element, so you could think of it as a queue or a ring buffer.
  • If the CALL stack is popped four times in a row, the fourth update to its copy of the PC is missed (the third PC update will be propagated). Probably a hardware bug.

Category:GPU