Surface formats: profile gates, renderer verdicts and format usage
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. Read at c6dec91f and re-checked at 3b43bd86; no test was run. Per-renderer format answers are code claims and several come from live device queries, so they can differ per GPU.
A SurfaceFormat value is a request, not a promise. This page explains how CNA decides whether a texture, a cube, a volume texture or a render target may use a format: the per-resource GraphicsProfile tables, the renderer's three-way verdict, the format-size helpers, the draw-time rules that XNA applies late, and the per-format usage masks in the renderer capability profile. It is for readers who need more than "Color is portable" and want to know exactly which call refuses what, and why.
Four separate claims about one format
The 27-value SurfaceFormat enum (SurfaceFormat.hpp) describes a wider contract than any single configured product implements: the 20 XNA 4.0 formats plus seven CNAEXT values (ColorBgraEXT, ColorSrgbEXT, Dxt5SrgbEXT, Bc7EXT, Bc7SrgbEXT, ByteEXT, UShortEXT). For one format CNA asks up to four independent questions, and a yes to one is never a yes to another:
- Does the enumerator exist? Always; that is representation only.
- Is there a transferable CPU representation? The
Color-shaped or typedSetData/GetDataroute (theColorTransferusage below). - Can the selected renderer store and sample it? Texture storage, per resource kind (2D, cube, volume).
- Can it be rendered into? Render-target capability is a strictly narrower question than storage, and blending into it or filtering it are narrower again.
A format enumerator, a transferable CPU representation, a sampleable native resource and a render target are four separate claims, and each is answered by a different function. The rest of this page follows the order in which a constructor asks them.
Format-size helpers
Texture (Texture.cpp) exposes as public static the size rules that FNA keeps internal, because callers outside the class hierarchy (content readers, the CNB compiler, renderers, tests) need the same byte and block arithmetic:
| Helper | Answer |
|---|---|
GetBlockSizeSquaredEXT(format) | 16 for the six block-compressed formats (Dxt1, Dxt3, Dxt5, Dxt5SrgbEXT, Bc7EXT, Bc7SrgbEXT: 4×4 texel blocks); 1 for the 21 uncompressed formats; System::ArgumentOutOfRangeException for a value outside the enum |
GetFormatSizeEXT(format) | Bytes per block or per texel: 8 for Dxt1; 16 for the other block formats and Vector4; 1 for Alpha8/ByteEXT; 2 for the packed 16-bit formats, HalfSingle, NormalizedByte2 and UShortEXT; 4 for Color and the other 32-bit formats; 8 for HalfVector4, Rgba64, Vector2 and HdrBlendable |
GetPixelStoreAlignment(format) | min(8, GetFormatSizeEXT(format)), the OpenGL 2.1 rule that GL_PACK_ALIGNMENT/GL_UNPACK_ALIGNMENT never exceed 8 |
ValidateGetDataFormat(format, elementBytes) | Throws System::ArgumentException when the destination element size does not evenly divide the format size |
A block-compressed level is allocated as ceil(w/4) × ceil(h/4) × GetFormatSizeEXT(format) bytes, never as w × h texels; the Texture2D format constructor applies exactly that rule when it zero-fills the initial image.
Texture2D admission, in order
The explicit-format constructor Texture2D(device, width, height, mipMap, format) in Texture2D.cpp runs five checks before any native allocation, in this order; the two-argument-size constructor runs only the first two and always creates Color.
- Profile size ceiling (
ValidateTextureSizeForProfileEXT): each edge at most the renderer'sGetMaxTextureSizeForProfileEXT, whose shared default is 2048 under Reach and 4096 under HiDef, and an aspect ratio of at most 2048:1. Both throwSystem::NotSupportedException; a zero or negative edge throwsSystem::ArgumentOutOfRangeExceptionbefore either is asked. These are profile ceilings, not hardware queries: a Reach game is held to 2048 on a GPU that could allocate more. - Renderer dimension (
ValidateTextureDimensionEXT):GraphicsDevice::GetMaxTextureDimension(), the renderer's real maximum, checked before creation because native validation layers are advisory or lazy (the source names advisory validation layers and wgpu-native's submit-time validation). A larger edge throwsSystem::NotSupportedException. - Profile format (
ValidateTexture2DProfileFormatEXT): the profile table; see the profile tables. - Creation shape (
ValidateTexture2DCreationShapeEXT): under Reach a non-power-of-two texture may be neither mipmapped nor DXT-compressed (NotSupportedException), and DXT dimensions must be multiples of four on both profiles (System::ArgumentException). - Renderer format (
ValidateTexture2DRendererFormatEXT): the renderer's verdict, asked only after every XNA rule above so that a renderer that cannot store a format never pre-empts the refusal XNA itself gives; see the renderer's three-way verdict.
A mipmapped texture gets the complete chain, halving each dimension until 1×1 (CalculateMipLevels); there is no partial chain.
Where the renderer maximum comes from
GraphicsDevice::GetMaxTextureDimension() forwards to IGraphicsRenderer::GetMaxTextureDimension() (IGraphicsRenderer.hpp). The interface default returns 16384. Its header comment justifies the value as Direct3D feature level 11_0's REQ_TEXTURE2D_U_OR_V_DIMENSION and the value that real Vulkan, Metal and desktop GL implementations report, and calls it guaranteed on every native API the project targets. It is a rationale, not a guarantee every family's API makes: an OpenGL ES 3.0 or WebGL 2 implementation is required to offer only 2048. At this snapshot one family overrides it, and only to return the same value.
| Family | What GetMaxTextureDimension() returns |
|---|---|
FNA3D | The interface default, returned explicitly. FNA3D has no maximum-texture-size query, and the source comment argues that inventing a narrower number would reject textures the renderer can create (Fna3dRenderer.cpp) |
Every other family inherits the constant, including the three EasyGL identities, DIRECTX9, SDL_GPU, WEBGPU and SOFTWARE. No family queries the device for this value, so on every family it is a documented assumption rather than a device fact: a GL or GLES device with a smaller GL_MAX_TEXTURE_SIZE still reports 16384, as the header comment's "no renderer currently needs a tighter or looser override" implies.
The profile ceiling (2048 under Reach, 4096 under HiDef) is checked first. For that reason the renderer maximum limits an ordinary Texture2D only when a family reports less than its profile allows. The value matters most in two other places:
- Content and stream loading. The XNB
Texture2DandSpriteFontreaders check declared sizes against it (Texture2DContentTypeReader.cpp). The DDS decoder behindDDSFromStreamEXTrefuses a larger header before it decodes anything (Texture2D.cpp). - Capability profile. The profile publishes the value as
RendererLimit::MaxTextureDimensionand marks it known whenever it is non-negative (GraphicsDevice.cpp). On an inheriting family, a "known" 16384 is therefore the interface default and was not reported by the device.
The practical rule: on every family, FNA3D included, treat the answer as an upper bound that the driver may still refuse. Checked by reading the sources above at c6dec91f and re-checked at 3b43bd86; not executed.
The profile tables, per resource kind
XNA decides format legality by GraphicsProfile. CNA's tables in Texture.cpp say they were measured on the real XNA 4.0 runtime rather than transcribed from documentation, and they differ per resource kind. The profile is always asked first and refuses with XNA's own exception type, so a profile refusal is distinguishable from a renderer refusal.
| Resource | Helper | Reach | HiDef |
|---|---|---|---|
Texture2D | IsFormatAllowedByProfileEXT | Nine formats: Color, Bgr565, Bgra5551, Bgra4444, Dxt1/Dxt3/Dxt5, NormalizedByte2/NormalizedByte4. The other eleven XNA formats throw NotSupportedException. | All 20 XNA formats |
TextureCube | IsCubeFormatAllowedByProfileEXT | Everything except NormalizedByte2/NormalizedByte4, at both profiles. Deliberately not the full Reach list: enforcing it would refuse a float cube on the default profile, and CNA's image-based-lighting products render irradiance and prefiltered cubes into float storage face by face (the source records this as an owner decision that the gate must not reopen). | |
Texture3D | IsVolumeFormatAllowedByProfileEXT | Nothing: volume textures are HiDef-only | Fifteen uncompressed formats (the 20 minus the three DXT formats and the two signed-normalized ones) |
| Render targets | IsRenderTargetFormatAllowedByProfileEXT | The Texture2D list minus Dxt1/Dxt3/Dxt5, which nothing renders into. A profile-excluded format is substituted, not refused (see render-target selection). | |
The CNAEXT formats are "not the profile's business": IsFormatAllowedByProfileEXT returns true for them at both profiles, and the renderer alone accepts or refuses them.
The renderer's three-way verdict
After the profile, the constructor asks the active renderer. RendererFormatVerdict in IGraphicsRenderer.hpp has three values, and the third is the important one:
Supported: the renderer genuinely stores this format.Unsupported: the renderer genuinely does not. ForTexture2Dthis throwsSystem::InvalidOperationExceptionnaming the ordinal and saying the format "has not passed the renderer's promotion gate"; for cubes, volumes and render targets it throwsNotSupportedException.Defer: the renderer has no renderer-specific answer, so the framework rule applies. That rule,Texture::ValidateFormat, admitsSurfaceFormat::Colorand throwsSystem::InvalidOperationException("... is not implemented by the selected graphics renderer") for anything else.
The tri-state exists because a plain Boolean would force every renderer without format work to restate the framework rule, and a wrong restatement would silently narrow a public API. The questions are separate virtuals because the answers really differ per resource kind:
| Virtual | Asked by | Default |
|---|---|---|
ClassifySurfaceFormatEXT | Texture2D | Defer |
ClassifyTextureCubeFormatEXT | TextureCube | Delegates to the 2D answer (so adding it changed no renderer); overridden where cube allocation differs |
ClassifyTexture3DFormatEXT | Texture3D | Defer, deliberately not the 2D answer: most volume paths store RGBA8 only |
ClassifyRenderTargetFormatEXT | RenderTarget2D, SupportsSurfaceFormatAsRenderTargetEXT | Defer |
ClassifyRenderTargetCubeFormatEXT | RenderTargetCube | Delegates to the render-target answer |
ClassifyColorTransferFormatEXT | The capability profile's ColorTransfer bit | Defer; framework rule: any format whose texel size is a multiple of four bytes |
The cube question exists because of a concrete failure recorded in TextureCube.cpp and the renderer interface as WEBGPU-163: one classifier served both kinds, so a block-compressed cube was promised at construction and then refused by every SetData. The WebGPU cube path was later made to allocate the requested format, and the cube verdict now repeats the 2D one including its device-feature gate. The colour-transfer question exists for the reverse reason: SDL_GPU refuses a Color-shaped transfer over NormalizedByte4 (four bytes wide, but signed and sampled in [-1, 1]) and over the DXT formats (whose bytes are blocks, not texels), although the width rule would admit both.
Families that classify formats themselves, and what they accept, are summarised on Renderers: texture and surface formats and per renderer on 3D rendering: surface format boundaries; every other family defers, which means Color only.
Cube and volume creation rules
TextureCube (TextureCube.cpp) refuses a zero or negative edge with System::ArgumentOutOfRangeException, then checks the profile's cube edge (GetMaxCubeSizeForProfileEXT: 512 under Reach, 4096 under HiDef), then the cube format table and verdict, then the shape: under Reach the edge must be a power of two, and a DXT cube edge must be a multiple of four. Texture3D (Texture3D.cpp) likewise refuses a zero or negative dimension first, then requires SupportsCapability(GraphicsCapability::Texture3D) and throws NotSupportedException before any renderer call otherwise, unless the device opted into Unsupported3DGraphicsCallBehavior::WarnAndStub, in which case the renderer's warn-once stub that reads back zeros answers instead; then GetMaxVolumeExtentForProfileEXT, whose 0 under Reach means "no volume textures at all" and whose HiDef value is 256 per axis; then the volume table and verdict. A mipmapped volume's level count halves all three dimensions, following XNA's complete-chain request rather than FNA's width/height-only helper, which would truncate a depth-dominant texture.
Render-target format selection
RenderTarget2D and RenderTargetCube take a preferred format, and SelectRenderTargetFormatEXT implements XNA's substitution: a format the profile forbids for targets, or one the renderer classifies Unsupported or Defer, becomes Color without an exception. The substituted format is then classified again inside CreateValidatedRenderTargetRenderer before the renderer factory runs, so the object can never report a format its native resource does not have (an earlier defect in which a target reported a non-Color format over Color storage is the reason, recorded as Task 774 in the constructor comment).
Because the substitution is silent, ask first. GraphicsDevice::SupportsSurfaceFormatAsRenderTargetEXT(format) (GraphicsDevice.cpp) asks exactly the constructor's questions in the same order (profile table, then ClassifyRenderTargetFormatEXT, with Defer meaning Color only), so the two cannot disagree; RenderTargetFormatAgreementTests.cpp pins that agreement for every SurfaceFormat. Reading getFormatProperty() after construction gives the same answer after the fact.
Rules checked at draw time
Some XNA format restrictions are not construction rules at all. Microsoft XNA checks them in VerifyCanDraw, when a draw is submitted, and CNA reproduces that timing in GraphicsDevice::validateDrawState for ordinary draws and SpriteBatch alike, before the renderer is called:
- Point-filter-only formats.
Single,Vector2,Vector4,HalfSingle,HalfVector2,HalfVector4andHdrBlendablemay be sampled only withTextureFilter::Point; any other filter on the slot throwsNotSupportedException("The active GraphicsProfile does not support filtering SurfaceFormat …"). HiDef can allocate these formats, but XNA's Direct3D 9 storage for them does not permit filtered sampling, so CNA refuses the draw even on renderers that could filter. - Non-blendable targets. With blending enabled or any colour-write mask other than
All, a bound target inSingle,Vector2,Vector4,HalfSingle,HalfVector2orHalfVector4throws.HdrBlendableis exempt, which is what its name promises. - Non-power-of-two under Reach. A non-power-of-two
Texture2Dsampled with any address mode other thanClampon U or V throws "Reach requires Clamp addressing for non-power-of-two Texture2D resources." - Vertex textures. The vertex-stage
TextureCollectionhas four slots under HiDef and none under Reach, and accepts only the seven float formats above (NotSupportedExceptionotherwise), matching XNA's vertex-texture-fetch rule.
The first three rules are pinned by GraphicsProfileDrawStateFormatTests.cpp. They explain an otherwise surprising result: a float texture that constructs successfully can still make the next draw throw.
Per-format usage in the capability profile
GraphicsDevice::GetRendererSurfaceFormatSupportEXT(format) returns a RendererFormatSupport with two masks over RendererFormatUsage bits (RendererCapabilityProfile.hpp): TextureStorage, Sampled, Filterable, RenderTarget, Blendable, TransferSource, TransferDestination, Mipmapped, Multisample and ColorTransfer. knownUsages says which bits have an answer at all; a missing known bit means "not classified", never "unsupported".
How BuildRendererCapabilityProfileEXT fills them matters when reading the answer:
- The device always classifies three bits for all 27 formats.
TextureStorageis the renderer'sClassifySurfaceFormatEXTverdict (Defercounting as supported only forColor); it does not include the profile gate, so under Reach it can report storage for a HiDef-only format that theTexture2Dconstructor would refuse.RenderTargetisSupportsSurfaceFormatAsRenderTargetEXTand does include the profile.ColorTransferisClassifyColorTransferFormatEXTwith the four-byte-multiple framework rule. - A renderer may add its own audited answers through
GetSurfaceFormatUsageSupportEXT; where it marks a bit known, its answer replaces the device's. At this snapshot the WebGPU family implements that hook; every other family reports only the three device-classified bits.
So a format answer is only as detailed as the selected renderer's audit. Treat an unknown Filterable or Blendable bit as a question to test, not as a no. The accessors and the report are introduced on Renderers: RendererCapabilityProfile.
Block-compressed content: decoded or native
Whether DXT and BC data stays compressed is decided by two renderer answers, not by the format alone. IsCompressedTransferFormatEXT(format) says the renderer transfers that format as raw blocks through the byte-array SetData/GetData overloads (the default is false for every format); LoadsCompressedContentNativelyEXT() says the renderer prefers loaded content to arrive compressed. Only when both are true do Texture2D::DDSFromStreamEXT and the .xnb Texture2D reader keep the blocks; otherwise they decode each level to RGBA8 Color on the CPU (DxtUtil), which is the historical behaviour every renderer that has not opted in keeps. Cubes have their own transfer question, IsCompressedCubeTransferFormatEXT, because a backend may implement compressed 2D textures without the cube allocation and upload path. The byte-level transfer rules for compressed textures are on Texture data transfer.
Color is linear: a worked historical case
SurfaceFormat::Color is linear RGBA8; the gamma-encoded variant is the separate CNAEXT value ColorSrgbEXT. A fixed WebGPU defect shows why the distinction is easy to break without noticing. The renderer once preferred an sRGB surface format, so a public Color request travelled with a native sRGB swapchain, and because the renderer copies its surface format into render targets, the encode also reached off-screen resources whose bytes a game reads back. A shared render-target-cube fixture even carried a WebGPU-only definition that sRGB-encoded its expectations, which turned the defect into the oracle. At this snapshot the renderer prefers the non-sRGB formats and, on a surface that offers only sRGB, renders through a non-sRGB view of the same bytes (WebGPU: surface format).
The lesson generalises. 0 and 255 are fixed points of the sRGB curve, so an exact-colour test that uses only those values cannot see a stray encode or decode, and a decode on sampling followed by an encode on presentation approximately cancels for textured content. A colour-space test needs a mid-range value and an untextured path, such as a mid-grey drawn once from a vertex colour and once from a sampled texture, with both readbacks required to agree.
Evidence and its limits
Checked by reading the CNA source at c6dec91f and re-checked at 3b43bd86; not executed. The profile tables, verdict order, substitution rule and draw-time checks are shared code, identical for every renderer, and are pinned by the unit tests named above (RenderTargetFormatAgreementTests.cpp, GraphicsProfileDrawStateFormatTests.cpp, the Texture2DTests.cpp format-acceptance cases). What a given renderer accepts after the profile is that renderer's claim: several families answer from live device queries (Direct3D 9 asks CheckDeviceFormat, and WebGPU gates the block-compressed formats on the device feature it enabled), so the same build can answer differently on another GPU. CNA's comments describe the profile tables as measured on the real XNA 4.0 runtime; this page did not repeat that measurement.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Graphics architecture: state and render targets
- Maintainer workflow
- Fix a renderer bug
- Tests and validation
- Test architecture
- Reference
- Test target index
- Deep dives
- Texture data transfer · Render target semantics