Skip to content
Lesson 5.6Executable Maps, Instruction References, and Scans

Instruction Reference: Scalar Floating-Point Values

Trace speed, position, numeric conversions, and NaN comparisons using the SSE instructions common in game code.

Lesson 5.6
Read or listenPrint chapter or book
About 20 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 5.6Key ideas, reference and questions

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 comiss updates 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
Download cheatsheet (.md)

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.

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.

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.

A scalar load from memory clears the upper 96 bitsThe float lands in the low 32 bits. Everything above it is set to zero.
  1. 1 Before: XMM0 holds earlier values

    XMM0
    earlier upper bitsupper 96 bits
    old floatlow 32 bits
    float in memory
    1.5

    The 128-bit register is split into the low 32 bits that a scalar float uses and the 96 bits above.

  2. 2 After the load: low = 1.5, upper bits cleared

    XMM0
    0 (cleared)bits 127 to 32
    1.5low 32 bits

    The 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.

A scalar copy from another XMM keeps the upper 96 bitsOnly the low 32 bits are replaced. The destination’s upper bits stay as they were.
  1. 1 Before: two registers with their own values

    XMM0 (destination)
    upper bits Aupper 96 bits
    old floatlow 32 bits
    XMM1 (source)
    upper bits Bupper 96 bits
    2.5low 32 bits

    XMM1 holds 2.5 in its low 32 bits.

  2. 2 After the copy: low = 2.5, upper bits A kept

    XMM0 (destination)
    upper bits A (preserved)upper 96 bits
    2.5low 32 bits

    The 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, xmm0
movss [shown_speed], xmm1

Move 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], xmm1

The 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
  1. 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).

  2. 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).

  3. 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], xmm0

Add 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], xmm0

Store 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
  1. 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).

  2. 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).

  3. 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], xmm0

Scale 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], xmm0

The 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
  1. 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).

  2. 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).

  3. 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.

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, eax
movss [score_float], xmm0

Convert 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], xmm0

Store 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
  1. 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.

  2. 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.

  3. 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.

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, xmm0
mov [score_int], eax

Truncate 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], eax

Store 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
  1. 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).

  2. 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).

  3. 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.

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 first
jb wait_for_speed ; ordered and below the limit
; otherwise: ordered and at least the limit

The 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: continue

The 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
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

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.

Knowledge check

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?

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.

Chapter 5: 11 lessons

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.