PackedVector Types
All 17 XNA 4.0 PackedVector types are real and implement their conversion surface. The half-float path is a correct IEEE-754 codec that handles subnormals, infinities and NaN in both directions. The one consistent shortfall: none of the 17 types implement ToString() or GetHashCode(), so do not rely on either for logging or hashed containers.
These are vertex formats, not texture formats, in practice. Texture::ValidateFormat throws for every SurfaceFormat except Color, so although the enum lists packed and float formats, you cannot construct a texture or render target in one. Use these types for vertex element data.
Overview
The Microsoft::Xna::Framework::Graphics::PackedVector namespace contains types that pack floating-point component values into compact integer representations. They are broadly useful in vertex layouts. Texture use is renderer-qualified: Skia promotes packed/normalized formats, IGL promotes only Rg32/Single, and other families retain the framework's Color-only public texture gate.
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)