CNA-GAP-009: Sub-pixel SpriteBatch destinations reach only EasyGL, OPENGL4 and SOFTWARE; every other renderer truncates them to whole pixels
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
modules/graphics/include/CNA/Internal/Renderers/Common/IGraphicsRenderer.hpp— ISpriteBatchRenderer float Draw default (quantise and forward)modules/graphics/src/Xna/SpriteBatch.cpp— renderer Draw call with destX/destY/destWidth/destHeightmodules/renderers/opengl4/src/OpenGL4SpriteBatch.cpp— OpenGL4SpriteBatchRenderer::Draw float override
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Deep dives
- SpriteBatch: sub-pixel destinations
- Known issues
- Functional gap index