CNA-BUG-002: BoundingSphere::Contains(const BoundingFrustum&) never returns Disjoint
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). 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. Nothing on this page was executed unless the Evidence section says so.
When any frustum corner lies outside the sphere, BoundingSphere::Contains(frustum) always answers Intersects, because its distance test compares a constant zero, so a frustum nowhere near the sphere is reported as overlapping.
- Identifier
CNA-BUG-002- Category
- Bug
- Subsystem
- Math & geometry
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Medium (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- ContainmentType BoundingSphere::Contains(const BoundingFrustum&) const; C ABI cna_bounding_sphere_contains_frustum
Expected behaviour
The header documents the function as checking "whether a frustum is outside, inside, or overlapping this sphere". XNA 4.0 first asks frustum.Intersects(sphere) and returns Disjoint when it is false, then returns Intersects if any of the eight frustum corners lies outside the sphere and Contains otherwise (read from the IL of the genuine XNA 4.0 Microsoft.Xna.Framework assembly; not executed).
Actual behaviour at TARGET
BoundingSphere.cpp returns Contains when every corner is inside. Otherwise it declares double dmin = 0.0; and immediately tests dmin <= Radius * Radius; nothing computes dmin, so the branch always answers Intersects and Disjoint is unreachable for any real radius. The code is a port of FNA's C# implementation (read at FNA commit 1358793, 2026-08-01), which carries the same unfinished branch under a // TODO : calcul dmin comment; CNA's port has no marker.
The neighbouring box overload, BoundingSphere::Contains(BoundingBox), computes the per-axis distance correctly and is not affected.
Source locations
modules/math/src/BoundingSphere.cpp— BoundingSphere::Contains(const BoundingFrustum&): dmin is declared and compared but never computedmodules/math/include/Microsoft/Xna/Framework/BoundingSphere.hpp— the documented Contains(const BoundingFrustum&) contractmodules/math/src/BoundingFrustum.cpp— BoundingFrustum::Contains(const BoundingSphere&, ContainmentType&): the working plane test in the other directionmodules/math/tests/Microsoft/Xna/Framework/BoundingSphereTests.cpp— BoundingSphereTest.ContainsFrustumReturnsNonContains asserts only that the answer is not Containsmodules/c-api/src/CnaCApiGeometry.cpp— cna_bounding_sphere_contains_frustum forwards to the member function
Evidence
Checked by reading at 009d40f5; nothing was built or executed for this entry. The unreachable branch is visible in the function body. The expected behaviour was taken from the XNA 4.0 IL; the FNA comparison shows where the gap came from.
Focused reproduction
Illustrative; not compiled or run for this entry.
BoundingSphere far(Vector3(1000.0f, 0.0f, 0.0f), 1.0f); // well outside the view volume
BoundingFrustum f(Matrix::CreateLookAt(Vector3(0.0f, 0.0f, 10.0f), Vector3::Zero, Vector3::Up) *
Matrix::CreatePerspectiveFieldOfView(MathHelper::PiOver4, 1.0f, 1.0f, 100.0f));
ContainmentType c = far.Contains(f);
// XNA: ContainmentType::Disjoint TARGET code: ContainmentType::Intersects
Current tests
BoundingSphereTest.ContainsFrustumReturnsNonContains in BoundingSphereTests.cpp uses a tiny sphere at the origin and asserts only EXPECT_NE(result, ContainmentType::Contains), which passes with the defect. The C ABI smoke test checks only the Contains case (a radius-100 sphere around a small frustum). No test expects Disjoint, and no XNA oracle covers bounding volumes other than CreateFromPoints.
Regression test
Three cases in BoundingSphereTests: a sphere far outside the frustum (expect Disjoint), a sphere overlapping one frustum face (expect Intersects), and a sphere enclosing all eight corners (expect Contains). Cross-checking the first two against frustum.Intersects(sphere) keeps the two directions consistent.
Blast radius
Affected: BoundingSphere::Contains(const BoundingFrustum&) (it has no output-parameter overload) and cna_bounding_sphere_contains_frustum. Not affected: the frustum-side tests BoundingFrustum::Contains(BoundingSphere) and BoundingFrustum::Intersects(BoundingSphere) (used by FrustumCullerEXT and ClusteredShadowPolicyEXT), and BoundingSphere::Intersects(const BoundingFrustum&), which delegates to the frustum. No in-tree module other than the C ABI calls the affected function.
Workaround
Ask the frustum first: treat !frustum.Intersects(sphere) as Disjoint and only then call sphere.Contains(frustum) to separate Contains from Intersects.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Math types: bounding types
- Maintainer workflow
- Add a regression test: the math case
- Known issues
- Bug index