CNA-BUG-054: HEADLESS reports occlusion-query support and a precise pixel count while its query always answers 1
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
modules/renderers/headless/src/HeadlessRenderer.cpp— HeadlessRenderer::SupportsCapability (default arm) and HeadlessRenderer::CreateOcclusionQuerymodules/renderers/headless/include/CNA/Internal/Renderers/Headless/HeadlessRenderer.hpp— HeadlessOcclusionQueryRenderer: End, IsComplete, PixelCount returning 1modules/graphics/include/CNA/Internal/Renderers/Common/IGraphicsRenderer.hpp— IOcclusionQueryRenderer::isPixelCountPreciseEXT default true; IGraphicsRenderer::SupportsCapability contract commentmodules/graphics/include/CNA/GraphicsCapability.hpp— GraphicsCapability::OcclusionQuery documentationmodules/graphics/include/Microsoft/Xna/Framework/Graphics/OcclusionQuery.hpp— isPixelCountPreciseEXT documentationmodules/graphics/tests/Microsoft/Xna/Framework/Graphics/OcclusionQueryPixelCountPrecisionTests.cpp— PixelCountMatchesWhatTheQuerySaysItIs skips HEADLESSmodules/graphics/tests/Microsoft/Xna/Framework/Graphics/GraphicsDeviceCapabilityTests.cpp— TheMultipleRenderTargetCapabilityMatchesWhatBindingActuallyDoes pins the MRT half
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Graphics renderers: capability matrix
- Architecture
- Graphics architecture: renderer families
- Tests and validation
- Test architecture: GPU tests
- Known issues
- Bug index