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

CNA snapshot 009d40f5  ·  Known Issues › Functional gaps  ·  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.

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

Identifier
CNA-GAP-009
Category
Functional gap
Subsystem
Graphics & renderers
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
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 EasyGLRenderer, OpenGL4SpriteBatchRenderer and the SOFTWARE sprite renderer override it; VULKAN, SDL_GPU, WEBGPU, the Direct3D renderers, DIRECT2D, GDI, SDL_RENDERER, the browser renderers, METAL, 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 009d40f5; 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 009d40f5; 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, OPENGL4, SOFTWARE); GDI's sprite batch wraps the Software 2D core but forwards only the three integer overloads, so it inherits the truncating default too. 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, OPENGL4 (parity corpus) and SOFTWARE; no other renderer registers it, so the truncating renderers have no sub-pixel pixel check.

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 one of the three renderers.

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