CNA-BUG-002: BoundingSphere::Contains(const BoundingFrustum&) never returns Disjoint

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

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

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.

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

Known issues
Bug index