CNA-BUG-054: HEADLESS reports occlusion-query support and a precise pixel count while its query always answers 1

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.

HEADLESS inherits OcclusionQuery = true and the default isPixelCountPreciseEXT() = true, but HeadlessOcclusionQueryRenderer completes at once and returns PixelCount() == 1 whatever was drawn.

Identifier
CNA-BUG-054
Category
Bug
Subsystem
Graphics & renderers
Status
Narrowed (partially fixed; describes only what survives)
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (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
CNA::GraphicsCapability::OcclusionQuery and OcclusionQuery::isPixelCountPreciseEXT() on the HEADLESS renderer

Expected behaviour

GraphicsCapability::OcclusionQuery is documented as "Real raster-sample occlusion queries (OcclusionQuery.Begin/End/PixelCount)", and the comment on IGraphicsRenderer::SupportsCapability requires every renderer with a narrower contract, "including no-renderer" ones, to override the applicable entries truthfully. OcclusionQuery::isPixelCountPreciseEXT() (CNAEXT) is true only "when the pixel count is a genuine per-fragment tally".

Actual behaviour at TARGET

HeadlessRenderer::SupportsCapability overrides only Texture3D, AdditiveBlending and MultiStreamVertexInput; OcclusionQuery reaches default: return true. HeadlessRenderer::CreateOcclusionQuery returns a HeadlessOcclusionQueryRenderer whose End() marks it complete and whose PixelCount() is the constant 1 (its comment calls this the conservative "everything visible" answer). It does not override isPixelCountPreciseEXT(), so the interface default true is reported. On a HiDef HEADLESS device a game sees occlusion queries advertised, a result available at once, PixelCount == 1 for any geometry (including none), and a claim that 1 is an exact fragment tally.

Source locations

Evidence

Checked by reading at 009d40f5; not executed. The rest of the original finding is fixed: HeadlessRenderer::SetRenderTargets binds a whole set into boundRenderTargets_ instead of refusing counts above one; SOFTWARE answers MRT and occlusion explicitly and backs them with four CPU colour attachments (SOFTWARE-120, Software_MrtStockEffectContract) and an exact CPU query (SOFTWARE-122, Software_OcclusionQuery_ExactContract); WebGPU binds two to four targets (WEBGPU-85/86/87, WebGPU_MRT, WebGpuMrtOcclusionContract.TwoTargetBindSucceedsAndBothTargetsReceiveOwnContent); and the shared GraphicsDeviceCapabilityTest.TheMultipleRenderTargetCapabilityMatchesWhatBindingActuallyDoes now attempts a two-target bind and compares it with the capability. The surviving HEADLESS half is visible in CNA's own test: OcclusionQueryPixelCountPrecisionTest.PixelCountMatchesWhatTheQuerySaysItIs skips HEADLESS ("validates query lifetime but has no raster samples to count") instead of comparing the capability and the precision claim with the result. Whether HEADLESS should answer false or keep a documented bookkeeping query is a maintainer decision; what is recorded here is the contradiction with the two documented contracts.

Independently observed as a separate finding (merged): Of the multiple-render-target and occlusion-query capability false positives once recorded, the MRT one is gone; HEADLESS still answers OcclusionQuery true from its default branch while HeadlessOcclusionQueryRenderer::PixelCount() returns 1.

Focused reproduction

// Illustrative; not compiled or run for this entry. Build with CNA_GRAPHICS_RENDERER=HEADLESS.
using namespace Microsoft::Xna::Framework::Graphics;
GraphicsDevice device(GraphicsAdapter::getDefaultAdapterProperty(), GraphicsProfile::HiDef,
                      PresentationParameters());
bool advertised = device.SupportsCapability(CNA::GraphicsCapability::OcclusionQuery); // true
OcclusionQuery query(device);
query.Begin();
query.End();                                    // nothing was drawn
bool complete = query.getIsCompleteProperty();  // true at once
int pixels = query.getPixelCountProperty();     // 1
bool precise = query.isPixelCountPreciseEXT();  // true

Current tests

OcclusionQueryPixelCountPrecisionTests.cpp: XnaLifecycleRejectsUnavailableAndInvalidSequences runs on HEADLESS (lifecycle only); PixelCountMatchesWhatTheQuerySaysItIs skips it. GraphicsDeviceCapabilityTest.SupportsOcclusionQuery expects true for HEADLESS through its catch-all arm, so it pins the current answer. HEADLESS statistics count query creation (occlusionQueriesCreated), not results.

Regression test

Either make HEADLESS answer OcclusionQuery false (updating the capability test's expectation), or keep the bookkeeping query, override isPixelCountPreciseEXT() to false and let PixelCountMatchesWhatTheQuerySaysItIs run on HEADLESS instead of skipping: with precision false it asserts a count of at most 1, which the constant satisfies.

Blast radius

HEADLESS only. Games and test suites run under HEADLESS that branch on PixelCount, or compute coverage ratios such as a lens-flare fade, always take the "visible" path and get 1/area; capability-driven feature selection believes real queries exist. Renderers with real queries and renderers that answer false are unaffected.

Workaround

Under HEADLESS, do not treat PixelCount as coverage; check the active renderer before relying on occlusion results.

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

Tests and validation
Test architecture: GPU tests
Known issues
Bug index