Recklessly encoding int as float
Quiz: Does this loop end, and if it does, what is the value of k
after the loop? (assuming IEEE-754 and usual 32-bit semantics for int/float
)
Running this code in VS you get k=16777217
which equals 2^24+1
.
In other words, in line with the true intent of the quiz, float
can encode -exactly- (without any precission loss) all natural numbers up to 2^24
(included). Because float
encodes sign in a dedicated bit, this property holds true for negative values as well. So you could hypothetically encode any [-2^24..+2^24]
int
in a float
with a static/implicit cast.
I said hypothetically because generally speaking I would never do such thing. Carelessly casting between int
and float
(without an explicit floor
, ceil
, trunc
, round
, …) is often a sign of lousy coding, unless performance is at stake and you know what you’re doing very well.
However, I came across the situation in Unity recently, where the Vertex ID
node in their Shader Graph hands over SV_VertexID
as a float
(disappointed face :-P). I would’ve expected an uint/int
output or a float
that you could reinterpret-cast to retrieve the raw SV_VertexID
bits with asuint(). But nope. You are handed over a float
which seemingly carries (float)SV_VertexID
.
One may recklessly use this float
to index a StructuredBuffer
ignoring the way static-casting works. This is one case where ignorance is bliss, because the float
received exactly matches the original int
as long as the original value is <=2^24
. That is, as long as you are dealing with fewer than (roughly) 16.7 million vertices, which is usually the case.
I believe that Unity’s ShaderGraph doesn’t support int/uint
as In/Out
values between nodes, so I guess that the Vertex ID
and Instance ID
nodes just static-cast the corresponding SV_...
value to a float. But it would be better (in the pedantic sense) to reinterpret the bit pattern of the raw values with asfloat
and then let the user retrieve them with asint()/asuint()
.
reinterpret-cast between int/float in HLSL
A (loose) explanation of the 2^24 limit
This is the IEEE-754 standard for floating-point numbers, as described in Wikipedia:
The value of a float-encoded number is reconstructed as (-1)^S * M * 2^E
.
S
is the 1-bit sign.M
is the 23-bit mantissa, interpreted as1.xxxxx
(in binary).E
is the 8-bit exponent, used asE-127
where 127 is often called bias.
e.g., a power-of-2 integer number like 4 would be encoded as:
M=1.00
which is the binary representation of 4, with the point at the left-most 1.E=2
(+bias).
The restored number is 1.00 shl 2=4
.
We should be able to do the same for all power-of-2 integers until we max out E
.
For non-power-of-2 integers the process is similar. e.g., number 5:
M=1.01...00
.E=2
(+bias).
The restored number is now 1.01 shl 2=5
.
This works the same for all integer numbers as long as M
can hold the raw binary representation of the number. The tallest binary number that M
can hold is 23 consecutive 1s. That is: 1.11...11
(24x1s
in total). With E=23
(+bias) this equals 2^24-1
.
The next integer 2^24
would be encoded as 1.00...00
(24x0s
clamped to 23, but the trailing 0s are meaningless here). With E=24
(+bias) this equals 2^24
(the answer provided above!!).
But the next integer 1.{23x0s.1}
can’t be encoded in a 23-bit M
. So from 2^24
onwards, there is necessarily a loss. Some integers beyond 2^24
(such as powers-of-2) may be luckily encoded exactly by float
. But not all consecutive numbers will. Actually, 2^24+1
is the first integer that won’t.
Whoa! As always, apologies for any mistakes in any of my posts.
[EDIT] The same reasoning can be applied to double, where the mantisa M
is 52-bit long.
Hence double
can encode exactly all integers in the range [-2^53..+2^53]
.