How imulExtended Works in GLSL Signed Multiplication

The imulExtended function in GLSL computes the full 64-bit signed product of two 32-bit signed integers, splitting the resulting value across two separate 32-bit output variables. This article details the mathematical behavior, register allocation, two's complement sign extension, and hardware execution patterns that govern how imulExtended prevents precision loss without requiring native 64-bit integer support.

Function Signature and Register Allocation

In the OpenGL Shading Language (GLSL), multiplying two 32-bit signed integers yields a result that can span up to 62 bits, requiring up to 64 bits to represent accurately in two's complement form. The standard multiplication operator * truncates this result to the lower 32 bits. To preserve the complete product, imulExtended is defined with the following signature:

void imulExtended(int x, int y, out int msb, out int lsb);

The function accepts two 32-bit signed inputs (x and y) and returns the product split into two 32-bit signed outputs:

  • lsb (Least Significant Bits): Contains the lower 32 bits of the 64-bit product. Its bit pattern is identical to standard truncated multiplication (x * y).
  • msb (Most Significant Bits): Contains the upper 32 bits of the 64-bit product, including the final sign bit.

Two's Complement and Sign Handling

The critical difference between signed multiplication (imulExtended) and unsigned multiplication (umulExtended) lies in how the high 32 bits (msb) are calculated.

In two's complement representation, a negative 32-bit integer \(v\) mathematically represents the value \(v - 2^{32}\) when viewed as an unsigned 32-bit integer. If both inputs are treated simply as unsigned words \(X\) and \(Y\), the mathematical product expands as:

\[\text{Product} = (X - s_x \cdot 2^{32}) \times (Y - s_y \cdot 2^{32})\]

Where \(s_x\) and \(s_y\) represent the sign bits (1 if negative, 0 if positive) of \(x\) and \(y\). Expanding this formula yields:

\[\text{Product} = X \cdot Y - s_x \cdot Y \cdot 2^{32} - s_y \cdot X \cdot 2^{32} + s_x \cdot s_y \cdot 2^{64}\]

Because \(2^{64}\) overflows out of a 64-bit register, the lower 32 bits (lsb) are unaffected by the signs of the inputs—they remain identical to an unsigned multiply (\(X \cdot Y \pmod{2^{32}}\)). However, the upper 32 bits (msb) require sign correction:

  1. If \(x < 0\), \(y\) must be subtracted from the high 32 bits of the unsigned product.
  2. If \(y < 0\), \(x\) must be subtracted from the high 32 bits of the unsigned product.

imulExtended automatically performs this sign extension internally, ensuring that the bit in position 63 (the most significant bit of msb) correctly reflects the overall sign of the multiplication.

Hardware Execution

Modern GPU architectures generally process integer operations in 32-bit ALU lanes. When imulExtended is compiled into GPU machine code (such as SPIR-V OpSMulExtended or hardware-level assembly):

  1. Dual Instruction or Wide Multiply: GPUs often provide dedicated instructions, such as IMUL.WIDE or separate operations for high-signed multiply (MUL.HI.S32) and low multiply (MUL.LO).
  2. Register Allocation: The compiler binds msb and lsb directly to target 32-bit registers. The ALU computes the 64-bit internal accumulator state and routes the low and high halves to their respective register locations in parallel or within consecutive cycles.
  3. Absence of 64-Bit Data Types: This operation allows shaders running on hardware without native 64-bit integer datatypes (like int64_t from GL_ARB_gpu_shader_int64) to emulate full-precision 64-bit fixed-point and integer math using pairs of standard 32-bit registers.

Reconstructing the 64-Bit Integer

If native 64-bit integer types are available, the split output of imulExtended can be combined into a single signed 64-bit variable:

int msb;
int lsb;
imulExtended(a, b, msb, lsb);

// Reconstruct into int64_t
int64_t fullProduct = (int64_t(msb) << 32) | int64_t(uint(lsb));

The lsb must be cast to an unsigned integer before being cast to a 64-bit integer. This prevents the compiler from sign-extending the lower 32 bits into the upper 32 bits prior to the bitwise OR operation.