PackedVector Types

Microsoft::Xna::Framework::Graphics::PackedVector — compact integer representations for GPU vertex and texture data

All 17 XNA 4.0 PackedVector types are fully implemented with 100% unit test coverage. Every type correctly handles edge cases including overflow, underflow, NaN, and signed/unsigned normalization boundary values.

Overview

The Microsoft::Xna::Framework::Graphics::PackedVector namespace contains types that pack floating-point component values into compact integer representations. These types are used to describe vertex buffer layouts and texture formats, reducing memory bandwidth on the GPU.

Each packed type stores its data in the smallest integer that can hold all components. The GPU can decode them natively in hardware, so pack-on-CPU, unpack-in-shader is both fast and lossless within the precision of each format. Using packed types instead of raw float arrays can cut vertex buffer size by 50–75% for typical 3D meshes.

All 17 types are available in the Microsoft::Xna::Framework::Graphics::PackedVector namespace in CNA, matching the XNA 4.0 public API exactly. Two additional names, Vector2 and Vector4, appear in the XNA 4.0 PackedVector namespace as type aliases; in CNA these resolve to Microsoft::Xna::Framework::Vector2 and Microsoft::Xna::Framework::Vector4 respectively.

IPackedVector interface

Every packed type implements the IPackedVector<T> interface, where T is the underlying integer storage type (uint8_t, uint16_t, uint32_t, or uint64_t). The interface defines three members:

Member Description
T PackedValue Gets or sets the raw packed integer. Reading this gives the bit-exact stored value; writing it replaces all components simultaneously.
void Pack(value) Converts a floating-point vector (or scalar) into the packed representation and stores the result in PackedValue. Component values outside the representable range are clamped.
Vector4 ToVector4() Unpacks the stored value and returns it as a Vector4. Components not present in the format are filled in as 0 (X, Y, Z) or 1 (W) to match XNA 4.0 behaviour.

Types with a single component also implement the non-generic IPackedVector base interface, which exposes only ToVector4(). This allows generic rendering code to unpack any packed type to Vector4 without knowing the concrete type.

Complete type reference

Type Storage Components Range Notes
Alpha8 uint8 1 × unorm8 0..1 Single alpha channel; maps 0–255 to 0.0–1.0
Bgr565 uint16 B5G6R5 0..1 Blue 5 bits, Green 6 bits, Red 5 bits; compact opaque colour
Bgra4444 uint16 B4G4R4A4 0..1 4 bits per channel; very compact RGBA for low-precision textures
Bgra5551 uint16 B5G5R5A1 0..1 1-bit alpha (fully opaque or fully transparent)
Byte4 uint32 4 × uint8 0–255 RGBA byte quad; raw unsigned integers, not normalized
HalfSingle uint16 1 × float16 ±65504 IEEE 754 half-float; see Half-float section below
HalfVector2 uint32 2 × float16 ±65504 X and Y components packed as two consecutive float16 values
HalfVector4 uint64 4 × float16 ±65504 X, Y, Z, W packed as four consecutive float16 values
NormalizedByte2 uint16 2 × snorm8 -1..1 Signed normalized; -128 maps to -1.0, 127 maps to +1.0
NormalizedByte4 uint32 4 × snorm8 -1..1 Common for vertex normals and tangents in compact vertex streams
NormalizedShort2 uint32 2 × snorm16 -1..1 Higher precision than NormalizedByte2; -32768 maps to -1.0
NormalizedShort4 uint64 4 × snorm16 -1..1 Four signed normalized 16-bit components
Rg32 uint32 2 × unorm16 0..1 Two unsigned normalized 16-bit components; 0–65535 maps to 0.0–1.0
Rgba1010102 uint32 R10G10B10A2 0..1 10 bits per RGB channel, 2-bit alpha; HDR-friendly wide-gamut format
Rgba64 uint64 4 × unorm16 0..1 Wide-format RGBA; each channel maps 0–65535 to 0.0–1.0
Short2 uint32 2 × int16 -32768..32767 Raw signed 16-bit integers; not normalized
Short4 uint64 4 × int16 -32768..32767 Four raw signed 16-bit integers; common for bone indices in skinning

IEEE 754 half-float (float16)

The three half-float types — HalfSingle, HalfVector2, and HalfVector4 — use the 16-bit IEEE 754 binary16 format, commonly called half-float or fp16. CNA implements the full conversion path in C++23 without relying on compiler extensions or hardware intrinsics, so the code is portable across all supported platforms.

Format layout

Field Bits Position Description
Sign 1 15 0 = positive, 1 = negative
Exponent 5 14–10 Biased by 15; exponent 0 = subnormal, 31 = infinity/NaN
Mantissa 10 9–0 Fractional bits; implicit leading 1 for normal numbers

The representable normal range is approximately ±6.10×10-5 to ±65504. The smallest positive subnormal is approximately 5.96×10-8.

HalfTypeHelper

All float32 ↔ float16 conversions go through the internal HalfTypeHelper class. It handles every case defined by the IEEE 754-2008 standard:

  • Normal numbers — re-biased exponent and rounded mantissa (round-to-nearest-even).
  • Subnormals — exponent field is 0, mantissa is non-zero; correctly decoded during unpack.
  • Positive and negative zero — sign bit preserved; both ±0 round-trip correctly.
  • Positive and negative infinity — exponent field all-ones, mantissa zero; preserved on pack and unpack.
  • NaN — exponent field all-ones, mantissa non-zero; both quiet and signaling NaN are packed as quiet NaN (all mantissa bits set to 1) to avoid platform-dependent signaling behaviour.
  • Overflow — float32 values with magnitude greater than 65504 saturate to ±infinity in the float16 result.
  • Underflow — float32 values smaller than the smallest subnormal flush to ±zero.
Method Signature Description
Pack uint16_t Pack(float value) Converts a 32-bit float to a 16-bit half-float bit pattern.
Unpack float Unpack(uint16_t value) Converts a 16-bit half-float bit pattern back to a 32-bit float.

Code examples

Creating and packing a HalfVector4

// Pack four floats into 64 bits (4 x float16)
HalfVector4 hv4;
hv4.Pack(Vector4(1.0f, 0.5f, -0.25f, 2.0f));

// Access the raw bit pattern
uint64_t bits = hv4.PackedValue;

// Unpack back to float
Vector4 result = hv4.ToVector4();
// result.X == 1.0f, result.Y == 0.5f, result.Z == -0.25f, result.W == 2.0f

Using NormalizedByte4 for a vertex normal

NormalizedByte4 is a compact format for storing unit normals and tangents in vertex buffers. Each component uses a signed 8-bit integer mapped to the -1..1 range, cutting normal storage from 12 bytes to 4 bytes per vertex.

// Pack a vertex normal (unit vector) into 4 bytes
NormalizedByte4 packedNormal;
packedNormal.Pack(Vector4(0.0f, 1.0f, 0.0f, 0.0f));  // pointing up

// Use as part of a vertex declaration
VertexElement normalElem(
    offsetof(MyVertex, Normal),
    VertexElementFormat::Byte4,
    VertexElementUsage::Normal,
    0
);

// Unpack on retrieval
Vector4 normal = packedNormal.ToVector4();
// normal.X == 0.0f, normal.Y == 1.0f, normal.Z == 0.0f

Using Rgba1010102 for HDR colour data

Rgba1010102 packs red, green, and blue each into 10 bits (0..1023 per channel, normalized to 0.0..1.0) and alpha into 2 bits. This provides roughly 3× the per-channel precision of Bgra5551 in the same 32-bit footprint, making it suitable for HDR render targets and wide-gamut textures.

// Pack an HDR colour (values above 1.0 are clamped by the packer)
Rgba1010102 hdrColor;
hdrColor.Pack(Vector4(0.95f, 0.72f, 0.12f, 1.0f));

// The 2-bit alpha maps 0.0..1.0 to 0..3
// The 10-bit RGB channels give ~0.001 precision per channel
Vector4 unpacked = hdrColor.ToVector4();

Checking HalfSingle precision and special values

// Normal value round-trip
HalfSingle hs;
hs.Pack(1.5f);
float v = hs.ToVector4().X;  // == 1.5f exactly (representable in float16)

// Infinity is preserved
hs.Pack(std::numeric_limits<float>::infinity());
// hs.PackedValue == 0x7C00 (positive infinity in float16)

// Values beyond float16 range saturate to infinity
hs.Pack(70000.0f);
// hs.PackedValue == 0x7C00 (overflow -> +inf)

// NaN is preserved as quiet NaN
hs.Pack(std::numeric_limits<float>::quiet_NaN());
// hs.PackedValue has exponent=0x1F and non-zero mantissa

// Zero sign is preserved
hs.Pack(-0.0f);
// hs.PackedValue == 0x8000 (negative zero in float16)