Instruction Reference: Scalar Floating-Point Values
Trace speed, position, numeric conversions, and NaN comparisons using the SSE instructions common in game code.
Cheatsheet for Lesson 5.6
A one-page summary of “Instruction Reference: Scalar Floating-Point Values” you can keep beside your notes. Plain Markdown.
Lesson 5.6 — Instruction Reference: Scalar Floating-Point Values: cheatsheet
- Remember: Track the low float, the untouched upper bits, and the kind of flags involved. A 128-bit register does not mean that this instruction uses every bit as one large number.
Quick reference
- Copy one float:
movss; upper-bit behavior depends on memory versus register source. - Add or scale a float:
addss/mulss; binary32 rounding and MXCSR status apply. - Integer to float:
cvtsi2ss; large integers may round. - Float to signed integer:
cvttss2si; truncate toward zero and handle invalid/out-of-range input. - Compare floats:
comiss; handle unordered/exception behavior before equality or ordering. - Understand flag provenance: Arithmetic SSE status lives in MXCSR; completed
comissupdates ZF/PF/CF. - Understand register width: Scalar operations use one low 32-bit value inside 128-bit XMM storage.
Review
- Q: A scalar single-precision operation uses an XMM register. How much of that register holds the numeric value being calculated? A: One low 32-bit float inside the wider register storage
- Q: Two legacy scalar float copies have the same low value, but one loads from memory and one copies another XMM register. Why inspect their forms? A: Their rules for the destination upper bits are different
Gold and ammo are often integers. Speed, position, and time are often floating-point numbers, which can represent fractions. This reference follows one float at a time in a fictional owned, resettable game. Every trace starts with a worked example; changing it is optional exploration.
These examples use the legacy two-operand SSE instructions in 32-bit x86, with Intel syntax. Their v... AVX relatives have different operand and upper-register rules. A real instruction reference must match the encoding you are actually reading. The links below point to the corresponding entries in revision 093 of Intel’s official instruction manual.
Find an operation
Section titled “Find an operation”| Instruction | Worked example | Intel entry |
|---|---|---|
movss |
Copy one speed value | MOVSS |
addss |
Add a movement step | ADDSS |
mulss |
Scale speed | MULSS |
cvtsi2ss |
Convert integer points to float | CVTSI2SS |
cvttss2si |
Truncate float points to integer | CVTTSS2SI |
comiss |
Compare speed with a limit | COMISS |
One float inside a wider register
Section titled “One float inside a wider register”An XMM register holds 128 bits. A binary32, or single-precision float, occupies 32 bits. Scalar means these instructions operate on one float in the low 32 bits, rather than calculating four floats together. The rest of the register still exists; do not assume every scalar instruction clears it.
The suffix ss stands for scalar single precision. Memory labels such as [speed] denote valid four-byte fields in our toy game. xmm below means an XMM register, m32 a four-byte memory source, and r32 a general-purpose 32-bit register.
Floating-point encoding differs from signed integer encoding. Copying the four bytes of an integer does not convert its numeric value into a float. A conversion calculates a new representation. Some values need rounding because a fixed-size format cannot represent every number exactly.
MXCSR is a control/status register for SSE floating-point work. It records rounding direction, exception masks, and floating-point status. It is separate from integer ZF/SF/CF/OF. The arithmetic traces use round-to-nearest, ties-to-even, and ordinary finite binary32 inputs. They show selected effects, not a complete floating-point environment.
Copies and arithmetic
Section titled “Copies and arithmetic”Meaning: copy one binary32 value between a low XMM field and another register or memory.
Legacy forms: movss xmm, xmm/m32, movss m32, xmm. Effects: register-to-register copies replace the low 32 bits and preserve the destination’s upper 96 bits. A memory load into XMM clears those upper 96 bits. A memory store writes only four bytes. Integer arithmetic flags are unchanged.
1 Before: XMM0 holds earlier values
XMM0earlier upper bitsupper 96 bitsold floatlow 32 bitsfloat in memory1.5The 128-bit register is split into the low 32 bits that a scalar float uses and the 96 bits above.
2 After the load: low = 1.5, upper bits cleared
XMM00 (cleared)bits 127 to 321.5low 32 bitsThe load writes the float and zeroes the other 96 bits.
The float lands in the low 32 bits. Everything above it is set to zero.
1 Before: two registers with their own values
XMM0 (destination)upper bits Aupper 96 bitsold floatlow 32 bitsXMM1 (source)upper bits Bupper 96 bits2.5low 32 bitsXMM1 holds 2.5 in its low 32 bits.
2 After the copy: low = 2.5, upper bits A kept
XMM0 (destination)upper bits A (preserved)upper 96 bits2.5low 32 bitsThe upper bits of XMM0 are preserved, so stale data can stay in them.
Only the low 32 bits are replaced. The destination’s upper bits stay as they were.
movss xmm0, [speed]movss xmm1, xmm0movss [shown_speed], xmm1Move one floating-point speed value. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
movss xmm0, [speed]
movss xmm1, xmm0
movss [shown_speed], xmm1The memory store writes just four bytes, representing 2.5. It does not store all 128 bits of xmm1.
Final values: speed = 2.5; xmm0 low = 2.5; xmm0 upper 96 = zero; xmm1 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; xmm1 low = 2.5; shown_speed = 2.5.
Read every step of the lesson’s example
Step 1, line 1. This legacy movss memory load reads one binary32 value into xmm0 low bits and clears its upper 96 bits. Integer comparison flags stay unchanged.
speed = 2.5; xmm0 low = 2.5; xmm0 upper 96 = zero; xmm1 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Step 2, line 2. The register-to-register movss copies only the low value into xmm1. Its upper 96 bits retain their previous contents.
speed = 2.5; xmm0 low = 2.5; xmm0 upper 96 = zero; xmm1 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; xmm1 low = 2.5.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Step 3, line 3. The memory store writes just four bytes, representing 2.5. It does not store all 128 bits of xmm1.
speed = 2.5; xmm0 low = 2.5; xmm0 upper 96 = zero; xmm1 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; xmm1 low = 2.5; shown_speed = 2.5.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Takeaway: distinguish a scalar load, register copy, and store before describing the rest of the register.
Meaning: add the source’s low float to the destination’s low float.
Legacy form: addss xmm, xmm/m32. Effects: write the low 32-bit floating result, rounded according to MXCSR; preserve the upper 96 XMM bits and integer arithmetic flags. It may record or raise floating-point exceptions. It does not write source memory.
movss xmm0, [position]addss xmm0, [movement_step]movss [position], xmm0Add a floating-point movement step. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
movss xmm0, [position]
addss xmm0, [step]
movss [position], xmm0Store the new position 3. SIMD floating-point status in MXCSR is separate from the ZF/SF/CF/OF shown here.
Final values: position = 3; step = 0.5; xmm0 low = 3; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Read every step of the lesson’s example
Step 1, line 1. Load position 2.5. This trace shows the low float; the memory load cleared xmm0's upper 96 bits.
position = 2.5; step = 0.5; xmm0 low = 2.5; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Step 2, line 2. addss computes 2.5 + 0.5, rounded to binary32 3. Its upper 96 bits and integer comparison flags do not change.
position = 2.5; step = 0.5; xmm0 low = 3; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Step 3, line 3. Store the new position 3. SIMD floating-point status in MXCSR is separate from the ZF/SF/CF/OF shown here.
position = 3; step = 0.5; xmm0 low = 3; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 40 (binary32 byte); +3 = 40 (binary32 byte).
Takeaway: float addition does not produce an integer carry flag for a following jc.
Meaning: multiply the destination’s low float by the source’s low float.
Legacy form: mulss xmm, xmm/m32. Effects: write the low binary32 result using MXCSR rounding; preserve the upper 96 bits and integer arithmetic flags. Floating-point status and possible exceptions belong to MXCSR.
movss xmm0, [speed]mulss xmm0, [speed_factor]movss [speed], xmm0Scale a floating-point speed. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
movss xmm0, [speed]
mulss xmm0, [factor]
movss [speed], xmm0The stored speed is 5. This float multiplication does not produce an integer carry flag.
Final values: speed = 5; factor = 2; xmm0 low = 5; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Read every step of the lesson’s example
Step 1, line 1. Load a binary32 speed into the low part of xmm0. The rounding mode is nearest, ties-to-even.
speed = 2.5; factor = 2; xmm0 low = 2.5; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Step 2, line 2. mulss computes 2.5 * 2 = 5 as binary32. Integer comparison flags and the upper 96 XMM bits stay unchanged.
speed = 2.5; factor = 2; xmm0 low = 5; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 20 (binary32 byte); +3 = 40 (binary32 byte).
Step 3, line 3. The stored speed is 5. This float multiplication does not produce an integer carry flag.
speed = 5; factor = 2; xmm0 low = 5; xmm0 upper 96 = zero; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = A0 (binary32 byte); +3 = 40 (binary32 byte).
Takeaway: changing a multiplier changes the computed speed; it does not change the encoding into an integer operation.
Numeric conversion
Section titled “Numeric conversion”cvtsi2ss
Section titled “cvtsi2ss”Meaning: convert a signed integer into one single-precision floating-point value.
Legacy 32-bit form: cvtsi2ss xmm, r32/m32. Effects: write the low binary32 value, preserve the upper 96 XMM bits and integer flags, and leave the integer source unchanged. MXCSR rounding applies when the integer cannot be represented exactly. For example, nearest, ties-to-even rounding converts 16,777,217 to 16,777,216.
mov eax, [score_int]cvtsi2ss xmm0, eaxmovss [score_float], xmm0Convert integer points into a float. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 7
cvtsi2ss xmm0, eax
movss [score_float], xmm0Store float 7. eax still holds integer 7; the legacy conversion preserved the upper 96 XMM bits.
Final values: case = small; eax = 7; xmm0 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; xmm0 low = 7; conversion exact = true; score_float = 7.
Read every step of the lesson’s example
Step 1, line 1. eax contains a signed integer. The bits are not yet a floating-point encoding.
case = small; eax = 7; xmm0 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. Convert the numeric value 7 to binary32 7. This value is represented exactly. The integer flags stay unchanged.
case = small; eax = 7; xmm0 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; xmm0 low = 7; conversion exact = true.
Step 3, line 3. Store float 7. eax still holds integer 7; the legacy conversion preserved the upper 96 XMM bits.
case = small; eax = 7; xmm0 upper 96 = previous contents; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; xmm0 low = 7; conversion exact = true; score_float = 7.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = E0 (binary32 byte); +3 = 40 (binary32 byte).
Takeaway: an integer that fits 32 bits does not necessarily fit exactly into a binary32 float.
cvttss2si
Section titled “cvttss2si”Meaning: convert a binary32 value into a signed integer by truncating toward zero.
Legacy 32-bit form: cvttss2si r32, xmm/m32. Effects: write the integer destination, preserve the float source and integer comparison flags. The tt form truncates regardless of MXCSR’s rounding direction. Thus 2.75 becomes 2, and −2.75 becomes −2.
NaN or an out-of-range result causes an invalid-operation condition. If the MXCSR invalid exception is masked, this 32-bit form writes the indefinite integer 0x80000000; if unmasked, it raises an exception. That bit pattern also represents the valid signed minimum, so checking only the output bits is insufficient. The trace explicitly assumes the invalid exception is masked.
movss xmm0, [score_float]cvttss2si eax, xmm0mov [score_int], eaxTruncate a float toward zero. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
movss xmm0, [score_float]
cvttss2si eax, xmm0
mov [score_int], eaxStore integer 2; the float source and integer comparison flags did not change.
Final values: case = positive; xmm0 low = 2.75; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; eax = 2; eax bits = 0x00000002; valid conversion = true; MXCSR invalid raised = false; score_int = 2.
Read every step of the lesson’s example
Step 1, line 1. Load binary32 2.75. The example has masked invalid-operation exceptions; an unmasked one would trap instead.
case = positive; xmm0 low = 2.75; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 30 (binary32 byte); +3 = 40 (binary32 byte).
Step 2, line 2. Truncate toward zero: 2.75 becomes 2. The TT form does not use the rounding direction for this conversion.
case = positive; xmm0 low = 2.75; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; eax = 2; eax bits = 0x00000002; valid conversion = true; MXCSR invalid raised = false.
Memory: +0 = 00 (binary32 byte); +1 = 00 (binary32 byte); +2 = 30 (binary32 byte); +3 = 40 (binary32 byte).
Step 3, line 3. Store integer 2; the float source and integer comparison flags did not change.
case = positive; xmm0 low = 2.75; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; eax = 2; eax bits = 0x00000002; valid conversion = true; MXCSR invalid raised = false; score_int = 2.
Memory: +0 = 02 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: choose the conversion’s rounding behavior deliberately and validate the numeric range.
Comparison and unordered values
Section titled “Comparison and unordered values”comiss
Section titled “comiss”Meaning: compare two low binary32 values and report their ordering in integer flags.
Legacy form: comiss xmm, xmm/m32. Effects: preserve both float operands; set ZF/PF/CF as below and clear OF/SF/AF when the comparison completes. A NaN is unordered, not less, equal, or greater. comiss signals invalid operation for a NaN. With that exception masked, it produces unordered flags; with it unmasked, the exception prevents the flag update.
| Completed comparison | ZF | PF | CF |
|---|---|---|---|
| left greater than right | 0 | 0 | 0 |
| left less than right | 0 | 0 | 1 |
| left equal to right | 1 | 0 | 0 |
| unordered, with invalid masked | 1 | 1 | 1 |
movss xmm0, [speed]comiss xmm0, [limit]jp reject_invalid ; unordered must be handled firstjb wait_for_speed ; ordered and below the limit; otherwise: ordered and at least the limitThe two labels name exit paths outside this excerpt. Notice that jp reads PF, which marks unordered here. Once unordered is excluded, unsigned-style jb/ja conditions can describe float ordering; signed-integer jl/jg do not interpret these flags correctly.
Compare floats, including an unordered value. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
movss xmm0, [speed]
comiss xmm0, [limit]
jp reject_invalid
jb wait_for_speed
; ordered and at least the limit: continueThe selected path is wait for speed. A float comparison needs its own flag interpretation.
Final values: case = below; xmm0 low = 2.5; limit = 3; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 0; unordered = false; below limit = true; outcome = wait for speed.
Read every step of the lesson’s example
Step 1, line 1. Load speed 2.5; the limit remains 3.0. NaN is not an ordered number.
case = below; xmm0 low = 2.5; limit = 3; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. comiss preserves both float operands and sets ZF=0, PF=0, CF=1. OF and SF are zero: do not use signed-integer jl/jg for this float ordering.
case = below; xmm0 low = 2.5; limit = 3; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 0.
Step 3, line 3. jp reads PF. PF is zero, so the ordered comparison proceeds.
case = below; xmm0 low = 2.5; limit = 3; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 0; unordered = false.
Step 4, line 4. jb reads CF. It jumps out to wait_for_speed because the ordered value is below the limit.
case = below; xmm0 low = 2.5; limit = 3; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 0; unordered = false; below limit = true.
Step 5, line 4. The selected path is wait for speed. A float comparison needs its own flag interpretation.
case = below; xmm0 low = 2.5; limit = 3; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 0; unordered = false; below limit = true; outcome = wait for speed.
Takeaway: a float comparison has four outcomes and its own flag rules.
Cheatsheet
Section titled “Cheatsheet”| Need | Instruction and limitation |
|---|---|
| Copy one float | movss; upper-bit behavior depends on memory versus register source. |
| Add or scale a float | addss/mulss; binary32 rounding and MXCSR status apply. |
| Integer to float | cvtsi2ss; large integers may round. |
| Float to signed integer | cvttss2si; truncate toward zero and handle invalid/out-of-range input. |
| Compare floats | comiss; handle unordered/exception behavior before equality or ordering. |
| Understand flag provenance | Arithmetic SSE status lives in MXCSR; completed comiss updates ZF/PF/CF. |
| Understand register width | Scalar operations use one low 32-bit value inside 128-bit XMM storage. |
Keep this reference beside Camera Frames and Projection and Coordinates, Vectors, and Directions. Their geometry depends on numeric values, not just the four bytes used to represent them.
Read the low scalar value
A scalar single-precision operation uses an XMM register. How much of that register holds the numeric value being calculated?
Scalar single-precision operations calculate one binary32 value in the low 32 bits. The rest of the 128-bit XMM register still exists and follows that instruction form's rules.
Finished Lesson 5.6?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.