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. Alpha.1 had one consistent shortfall — none of the types implemented ToString() or GetHashCode() — and this snapshot closes it: all 17 now have ToString(), a real GetHashCode(), Equals(std::any) and Equals(T), and IPackedVector gained ToVector4().
These are mainly vertex formats; texture use is doubly gated. A public Texture2D or render target in a packed or float SurfaceFormat has to pass two gates. First, the profile gate: the default Reach profile refuses Rgba1010102, Rg32, Rgba64, Alpha8, Single, Vector2, Vector4, HalfSingle, HalfVector2, HalfVector4 and HdrBlendable with NotSupportedException unless HiDef is requested. Second, the renderer gate: eight renderer families (DirectX 11, the EasyGL identities, SDL_GPU, WebGPU, Vulkan, Metal, FNA3D and Software) classify the broader formats, and each accepts its own subset, while the other four identities (SDL_RENDERER, DIRECTX9, HEADLESS and STUB) accept only SurfaceFormat::Color for public textures. Use these types for vertex element data first, and check the renderer reference before relying on a packed texture format.
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 profile- and renderer-qualified, as the note above explains.
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 interface, and through the typed IPackedVectorT<T> (the C++ spelling of XNA's IPackedVector<TPacked>) exposes its raw storage, where T is the underlying integer type (uint8_t, uint16_t, uint32_t, or uint64_t). Between them the interfaces define three members, spelled the C++ way:
| Member | Description |
|---|---|
T getPackedValueProperty() / setPackedValueProperty(T) |
Gets or sets the raw packed integer. Reading this gives the bit-exact stored value; writing it replaces all components simultaneously. |
void PackFromVector4(const Vector4&) |
Converts a Vector4 into the packed representation and stores the result in the packed value. Each type also has constructors from its component floats and from a Vector4. For the 14 integer-packed types, component values are saturated to the representable range, rounded ties-to-even, and NaN packs as 0 (measured against the genuine XNA 4.0 runtime); the three half-float types use the IEEE half codec described below. |
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. |
The non-generic IPackedVector base exposes PackFromVector4 and ToVector4() only, with no storage type. This allows generic rendering code to pack and unpack any packed type through a Vector4 without knowing the concrete type. Every packed type also provides ToString() (uppercase, zero-padded hexadecimal of the packed value for the 14 integer types; the decoded value for the three half types), GetHashCode(), Equals(std::any) and Equals(T), and the C++ generic families are stored as the host-language substitutions IPackedVectorT (for XNA's IPackedVector<T>) in the census.
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 | Four unsigned bytes X, Y, Z, W built from floats in [0, 255]; 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 HalfTypeHelper struct (an internal helper in XNA, a public header in CNA). 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. (This is the half-float behaviour; the 14 integer-packed types map NaN to 0 instead.)
- 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 |
|---|---|---|
Convert |
static uint16_t Convert(float value) |
Converts a 32-bit float to a 16-bit half-float bit pattern. |
Convert |
static float Convert(uint16_t value) |
Converts a 16-bit half-float bit pattern back to a 32-bit float. |
Code examples
Creating and packing a HalfVector4
using namespace Microsoft::Xna::Framework::Graphics::PackedVector;
// Pack four floats into 64 bits (4 x float16)
HalfVector4 hv4(Vector4(1.0f, 0.5f, -0.25f, 2.0f)); // or hv4.PackFromVector4(...)
// Access the raw bit pattern
uint64_t bits = hv4.getPackedValueProperty();
// 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 NormalizedShort4 for a vertex normal
NormalizedShort4 is the compact vertex format for unit normals and tangents. Each component uses a signed 16-bit integer mapped to the -1..1 range, cutting normal storage from 16 bytes (Vector4) or 12 bytes (Vector3) to 8 bytes per vertex. (NormalizedByte2 and NormalizedByte4 exist as XNA types, but XNA 4.0 has no VertexElementFormat for them; they are texture-data formats.)
// Pack a vertex normal (unit vector) into 8 bytes
NormalizedShort4 packedNormal(Vector4(0.0f, 1.0f, 0.0f, 0.0f)); // pointing up
// Use as part of a vertex declaration
VertexElement normalElem(
static_cast<int>(offsetof(MyVertex, Normal)),
VertexElementFormat::NormalizedShort4,
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 twice the bits per colour channel of Bgra5551 (10 rather than 5) in the same 32-bit footprint. Values are saturated, not carried above 1.0, so the format is wide-gamut rather than truly high-dynamic-range. As a texture or render target it needs the HiDef profile and a renderer that promotes it (see the note above).
// Pack a colour (values above 1.0 are saturated by the packer)
Rgba1010102 hdrColor(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();
Rounding: ties to even
Integer-packed conversions round half to even after saturating, matching XNA's runtime. The example uses Byte4, whose components are unsigned bytes built from floats in [0, 255]:
Byte4 b(0.5f, 1.5f, 2.5f, 300.0f);
// 0.5 -> 0 (tie to even), 1.5 -> 2 (tie to even), 2.5 -> 2 (tie to even), 300 saturates to 255
Vector4 v = b.ToVector4(); // (0, 2, 2, 255)
std::string hex = b.ToString(); // uppercase 8-digit hex of the packed value
Byte4 nan(std::numeric_limits<float>::quiet_NaN(), 0.0f, 0.0f, 0.0f); // NaN packs as 0
Checking HalfSingle precision and special values
// Normal value round-trip
HalfSingle hs(1.5f);
float v = hs.ToVector4().X; // == 1.5f exactly (representable in float16)
// Infinity is preserved
hs = HalfSingle(std::numeric_limits<float>::infinity());
// hs.getPackedValueProperty() == 0x7C00 (positive infinity in float16)
// Values beyond float16 range saturate to infinity
hs = HalfSingle(70000.0f);
// hs.getPackedValueProperty() == 0x7C00 (overflow -> +inf)
// NaN is preserved as quiet NaN (the half types do not map NaN to 0)
hs = HalfSingle(std::numeric_limits<float>::quiet_NaN());
// exponent == 0x1F and non-zero mantissa
// Zero sign is preserved
hs = HalfSingle(-0.0f);
// hs.getPackedValueProperty() == 0x8000 (negative zero in float16)
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Math value types in C++: object layout, equality, hashing and API shape — Which CNA math types carry a vtable, why Color is 24 bytes on 64-bit hosts, the internal vertex stream structs, output-reference aliasing, exact equality, hash and ToString differences, and the split argument exceptions.