Rectangle, Point and Color: integer geometry and packed colour semantics

CNA snapshot 009d40f5  ·  Deep Dives › Framework core  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page); oracle-compared (recorded by CNA, not re-run here). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read at 009d40f5 and compared with the decompiled XNA 4.0 assembly; the colour rounding rule rests on CNA's recorded XNA packing measurements, not re-run here.

Rectangle, Point and Color are the math types 2D games touch most, and their rules are the ones people guess wrong: which edges a rectangle owns, whether touching rectangles intersect, what "empty" means, how a colour's bytes are packed, how many named colours there really are and when a colour is premultiplied. This page gives the exact behaviour of each at TARGET, checked against the genuine XNA 4.0 implementation. It is for 2D gameplay, UI and tools code. The member tables are on Math types; colour rounding rules for maintainers are on Math internals: colour packing.

Rectangle edge ownership

Rectangle is four 32-bit integers, X, Y, Width and Height, with Y growing downward in screen use. getRightProperty() is X + Width and getBottomProperty() is Y + Height: the first column and row outside the rectangle. Every rule below follows from that half-open reading, and every one matches XNA 4.0 (Rectangle.cpp):

MemberRuleConsequence
Contains(x, y), Contains(Point)X <= x && x < X + Width && Y <= y && y < Y + HeightLeft and top edges belong to the rectangle, right and bottom edges do not. Rectangle(0, 0, 10, 10).Contains(10, 5) is false. A zero-width rectangle contains nothing.
Contains(Rectangle)the other rectangle's left/top ≥ this one's and its right/bottom ≤ this one'sA rectangle contains itself; containment of an empty rectangle depends only on its position.
Intersects(Rectangle)strict < on all four edgesRectangles that only touch along an edge or at a corner do not intersect. BoundingBox uses the opposite convention (touching boxes intersect).
Rectangle::Intersect(a, b)the overlap if there is one, otherwise all four fields zeroTouching rectangles give Rectangle::Empty, not a zero-width strip at the shared edge.
Rectangle::Union(a, b)the bounding rectangle of both, computed from X, Y, Right and BottomIt does not skip empty inputs: Union(Rectangle::Empty, r) stretches r to include the origin.
Inflate(h, v), Offset(x, y)X -= h, Width += 2h (and likewise for Y); Offset moves the locationNegative amounts shrink; nothing prevents a negative width.
getCenterProperty()Point(X + Width / 2, Y + Height / 2) in integer arithmeticAn odd width rounds the centre toward the left (integer division truncates toward zero).
getIsEmptyProperty()true only when X, Y, Width and Height are all zeroRectangle(5, 5, 0, 0) is not empty. To ask whether a region has area, test Width > 0 && Height > 0.

Like XNA 4.0, CNA's Rectangle has no Contains(Vector2) and no Size property (the location is getLocationProperty()/setLocationProperty(Point)); the float overloads some later frameworks added are absent. GetHashCode() XORs the four fields where XNA adds their hashes, so hash values differ from XNA's while equality is the same field-wise comparison. The arithmetic is plain int: X + Width on values near the integer limits overflows, which is undefined behaviour in C++ (XNA's unchecked C# arithmetic wraps).

Point and the rounding decision

Point is two 32-bit integers with Point::Zero, equality and ToString() {X:1 Y:2}. It has no bridge to Vector2 in either direction, as in XNA; write Vector2(static_cast<float>(p.X), static_cast<float>(p.Y)) for the widening direction. CNA's Point also has component-wise +, -, * and / (with no guard against dividing by zero), which XNA 4.0's Point does not have; they are not CNAEXT-marked, so the strict-API build does not flag them, and code meant to stay XNA-portable should not use them.

Going from a continuous coordinate to a pixel or grid cell is a rounding policy, and C++ makes the default silent. static_cast<int> truncates toward zero, so −0.5 becomes 0 and the cells −1 and 0 merge around the origin; std::floor gives −1, the cell that contains the point; std::lround(-0.5f) rounds half away from zero to −1, while std::nearbyint in the default mode rounds half to even, giving 0. Choose one per use (floor for cells, a round-to-nearest for pixel snapping) and write it out.

Color: the packed word and the object

Color stores one 32-bit word. Its numeric value is A<<24 | B<<16 | G<<8 | R, which the header calls AABBGGRR: red in bits 0–7, green 8–15, blue 16–23, alpha 24–31. On a little-endian machine the four bytes of that word appear in memory as R, G, B, A, which is why a Color's payload uploads as an RGBA8 texel or vertex colour. The word is endianness-independent as an integer; code that reads it byte by byte is not. A renderer, image path or binding that treats the word as ARGB swaps red and blue even though every getter looks right; ColorTest.RedPackedValueIsAabbggrr and its blue and CornflowerBlue siblings pin the order.

The packed word is the colour's value, not its storage: the C++ object also carries two vtable pointers (one per polymorphic base, IPackedVectorT<UInt32> and IEquatable<Color>) and is 24 bytes on a 64-bit host. Never reinterpret an array of Color as pixel bytes; read getPackedValueProperty() or the channels, and let the typed texture APIs convert. The full layout story is on Math value types: which types carry a vtable.

141 named colours, and three honest counts

Color.hpp declares 141 static const Color members and Color.cpp defines all 141 — the same set as XNA 4.0's 141 static Color properties, from AliceBlue to YellowGreen plus Transparent. Three numbers are each correct for a different question:

  • 141 named constants (declarations).
  • 140 opaque colours: Transparent (packed 0x00000000, transparent black) is the only constant whose alpha is not 255.
  • 139 distinct packed values: Aqua and Cyan are both 0xFFFFFF00 (R 0, G 255, B 255), and Fuchsia and Magenta are both 0xFFFF00FF (R 255, G 0, B 255).

All the opaque constants are stored straight, which for alpha 255 is the same as premultiplied. A named colour is API surface; it says nothing about how blending treats it.

Construction and arithmetic rules

RouteRule at TARGET
Color(float r, g, b[, a]), Color(Vector3), Color(Vector4), PackFromVector4Each unit value is multiplied by 255, saturated to 0–255 and rounded to nearest with ties to even; NaN packs as 0 and infinities saturate. The Vector3 and three-float forms set alpha to 255. This is the rule measured on the genuine XNA runtime (28 colour cases in CNA's framework packing oracle); a plain clamp-and-truncate, which FNA uses, gives 63 instead of 64 for 0.25.
Color(int r, g, b[, a])Each argument is clamped to 0–255; no rounding is involved. Integer literals select this overload.
Color(bytecs …) (CNAEXT), setPackedValueProperty, the channel settersStored as given.
Multiply(c, s), c * s, s * c (CNAEXT)Scales all four channels, alpha included, in float and truncates (as XNA does); a negative scale clamps to 0. Fading a premultiplied colour this way fades it correctly; fading a straight colour also darkens it.
Lerp(a, b, t)Clamps t to [0, 1], interpolates every channel and truncates: halfway between black and white is 127. A NaN amount yields 0 in every channel.
ToVector3(), ToVector4()Each byte divided by 255.
operator==, Equals, GetHashCode, ToStringCompare and hash the packed word; text is {R:100 G:40 B:20 A:128}, as in XNA. There are no +, - or / operators. Color never throws.

A default-constructed Color is transparent black (0, 0, 0, 0), as the XNA value type is.

Premultiplied alpha and the blend state

The type holds either convention; the bound BlendState decides how the channels are read. CNA's default is premultiplied, as in XNA 4.0: BlendState::AlphaBlend, which SpriteBatch::Begin uses when no blend state is given, blends with source factor One and destination factor InverseSourceAlpha, so it expects RGB already multiplied by alpha. BlendState::NonPremultiplied uses SourceAlpha / InverseSourceAlpha for straight colours. Color::FromNonPremultiplied converts:

const Color straight(200, 80, 40, 128);                                     // stored as (200, 80, 40, 128)
const Color premultiplied = Color::FromNonPremultiplied(200, 80, 40, 128);  // (100, 40, 20, 128)
const Color fromFloats = Color::FromNonPremultiplied(Vector4(0.8f, 0.3f, 0.2f, 0.5f));

The integer overload computes r * a / 255 with integer division (25600 / 255 = 100 for red) and keeps a; the Vector4 overload multiplies X, Y and Z by W and then uses the rounding float constructor. ColorTest.FromNonPremultipliedOverloadsAgreeWithXna shows both overloads agreeing for one input, (128, 0, 32, 128). One edge differs from XNA: XNA multiplies in 64-bit integers and clamps each result, while CNA's product is a 32-bit int, so arguments far outside 0–255 (a product beyond about 2.1 billion) overflow, which is undefined behaviour in C++. Keep the arguments in byte range. Drawing straight colours under the default state makes translucent sprites look too bright at their edges; Tutorial 7: transparency shows the practical fix, and Graphics state: BlendState lists the presets.

Evidence

Checked by reading TARGET at 009d40f5 (Rectangle.cpp, Point.cpp, Color.cpp, Color.hpp, BlendState.cpp and the tests RectangleTests, PointTests, ColorTests); the constant counts were taken from the declarations and definitions, and the XNA comparisons from the decompiled genuine Microsoft.Xna.Framework assembly. The colour rounding rule rests on CNA's recorded XNA measurements, reproduced by XnaFrameworkPackingTests in the graphics tree; nothing was run for this page. Packed-vector formats beyond Color are on PackedVector types.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture