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:
- If \(x < 0\), \(y\) must be subtracted from the high 32 bits of the unsigned product.
- 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):
- Dual Instruction or Wide Multiply: GPUs often
provide dedicated instructions, such as
IMUL.WIDEor separate operations for high-signed multiply (MUL.HI.S32) and low multiply (MUL.LO). - Register Allocation: The compiler binds
msbandlsbdirectly 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. - Absence of 64-Bit Data Types: This operation allows
shaders running on hardware without native 64-bit integer datatypes
(like
int64_tfromGL_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.