CNA-GAP-009: Sub-pixel SpriteBatch destinations reach only EasyGL and SOFTWARE; every other renderer truncates them to whole pixels

CNA snapshot 3b43bd86  ·  Known Issues › Functional gaps  ·  source links pinned to 3b43bd86

✓

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 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

SpriteBatch forwards float destinations through ISpriteBatchRenderer's sub-pixel Draw overload, whose default truncates toward zero into an integer Rectangle; only the EasyGL and SOFTWARE sprite renderers override it.

Identifier
CNA-GAP-009
Category
Functional gap
Subsystem
Graphics & renderers
Status
Open
Verified against
CNA 3b43bd86 (3b43bd8687ed46523d873f9bec4b32bd8481dcb5)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
SpriteBatch::Draw / DrawString with fractional positions or scales; ISpriteBatchRenderer::Draw(texture, float x, float y, float w, float h, ...)

Expected behaviour

XNA 4.0 carries sprite destinations as floats to the GPU, so a sprite at x = 10.5 lands between pixels and is filtered by the sampler (the interface comment in IGraphicsRenderer.hpp says the same).

Actual behaviour at TARGET

SpriteBatch calls the float overload for every queued sprite (SpriteBatch.cpp). The default implementation quantises each value with static_cast<int> (truncation toward zero, so -0.5 becomes 0) and forwards to the integer-Rectangle overload. Only EasyGLSpriteBatchRenderer and the SOFTWARE sprite renderer override it; SDL_GPU, WEBGPU, DIRECTX9, FNA3D and the rest draw on whole pixels. Smooth sub-pixel scrolling and fractional text scaling therefore snap on those renderers.

Source locations

Evidence

Checked by reading the named sources at 3b43bd86; nothing was built or executed for this entry. The set of overriding renderers was established by grepping renderer headers and sources for the float overload.

Independent re-verification: Checked by reading the named sources at 3b43bd86; nothing was built or executed for this entry. The set of overriding renderers was established by enumerating every ISpriteBatchRenderer implementation and grepping for the float overload (EasyGL, SOFTWARE). Test coverage: SpriteBatchSubPixelDestinationTest in SpriteBatchTests.cpp checks that SpriteBatch records fractional positions. spritebatch_subpixel_contract_test.cpp (SOFTWARE-137) is a renderer-neutral, byte-exact pixel fixture (a direct float draw must equal an integer draw under a transform matrix, which a truncating renderer fails) and is registered only for EasyGL and SOFTWARE, so the truncating renderers have no sub-pixel pixel check.

Re-verified at c6dec91f: a grep of every renderer for the float-destination overload finds only the EasyGL and SOFTWARE sprite renderers, and only their example trees register spritebatch_subpixel_contract_test.cpp. Not executed.

Focused reproduction

No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.

Current tests

SpriteBatchSubPixelDestinationTest in SpriteBatchTests.cpp checks that SpriteBatch records fractional positions; it does not check what each renderer draws.

Regression test

A renderer-neutral pixel fixture drawing a sprite at x + 0.5 with LinearClamp and asserting a blended edge column, registered for each family.

Blast radius

2D games with slow scrolling, sub-pixel camera motion or fractional text scale on the non-overriding renderers.

Workaround

Round positions yourself to get stable (if stepped) motion, or use an EasyGL identity or SOFTWARE.

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