PackedVector Types

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

ⓘ

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)