Tutorial 152: Reach vs HiDef: Choosing a GraphicsProfile
What you’ll learn: what a GraphicsProfile is and why Reach is the enforced default on every renderer; exactly which limits and features it gates (MRT, occlusion queries, 32-bit indices, float targets, large cubes and textures, GetBackBufferData and more); how to request HiDef with GraphicsDeviceManager, GraphicsAdapter and the project-wide setting; the exceptions you will see; and a probe program that tests all of it on your renderer.
Before you start — Tutorial 31: Your First 3D Triangle and Tutorial 38: Vertex Buffers and Index Buffers (32-bit indices are the most common thing that trips over the default profile), and a working CNA project as in Tutorial 03. Nothing here needs CNA_CNAEXT. You need a 3D-capable renderer to see the interesting half of the output.
Not the same thing as the capability profile. GraphicsProfile (Reach or HiDef) is XNA’s own two-value enum, and is what this tutorial is about. RendererCapabilityProfile, the report of what a renderer can do, is a separate CNAEXT API covered in Tutorial 133. The two interact (see Two layers), but they are different objects.
If you followed the older tutorials and something threw System::NotSupportedException mentioning “the Reach graphics profile”, this is the page for you. Every graphics device in CNA runs under a GraphicsProfile. In XNA that was a label chosen in the project file. In this CNA snapshot it is also an enforced ceiling: the shared device layer checks it on every renderer, and the default is Reach, the smaller of the two. At alpha.1 only the DIRECTX9 renderer enforced these ceilings, so code that ran on another renderer at alpha.1 (a 32-bit index buffer, a two-target render target, an occlusion query) can throw at this snapshot until you ask for HiDef.
What a profile is
Microsoft::Xna::Framework::Graphics::GraphicsProfile has two values. Reach is XNA’s “widest variety of devices” profile: a deliberately small feature set. HiDef is the “largest available set” profile. CNA applies the profile as a software ceiling, not as a hardware query: even on a GPU that could allocate a 4096-pixel texture, a Reach game is limited to 2048, which is exactly what XNA’s portability guarantee means. The profile lives on the GraphicsDevice (getGraphicsProfileProperty()) and is chosen through the GraphicsDeviceManager that creates the device.
The default is Reach, on every renderer
A GraphicsDeviceManager takes its starting profile from CNA::GetProjectGraphicsProfileEXT(), which is Reach until you declare otherwise (XNA embeds the profile in the project file; a C++ program has no such place, so CNA keeps it in a small project-wide setting). Nothing in your build configuration changes it: not the renderer you pick with CNA_GRAPHICS_RENDERER, and not a Debug or Release build.
What the profile gates
These are the checks made by CNA’s shared device layer, in the same place for every renderer. Where a limit is exceeded CNA throws System::NotSupportedException unless the last column says otherwise.
| What | Reach (the default) | HiDef |
|---|---|---|
Texture2D / RenderTarget2D edge length | 2048 | 4096 |
Mipmapped or DXT-compressed non-power-of-two Texture2D | refused | allowed |
Sampling a non-power-of-two Texture2D with a non-Clamp address mode | throws at draw time | allowed |
TextureCube / RenderTargetCube edge length | 512, power of two only | 4096 |
Texture3D (volume textures) | refused outright | up to 256 per axis; 15 uncompressed formats |
| Simultaneous render targets (MRT) | 1 | 4 |
32-bit IndexBuffer and 32-bit user indices | refused | allowed |
| Primitives in one draw call (and instances in one instanced call) | 65,535 | 1,048,575 |
OcclusionQuery | refused | allowed, if the renderer has queries |
SurfaceFormat of textures and render targets | only Color, Bgr565, Bgra5551, Bgra4444, Dxt1/Dxt3/Dxt5, NormalizedByte2/NormalizedByte4; the other eleven (Rgba1010102, Rg32, Rgba64, Alpha8, Single, Vector2, Vector4, HalfSingle, HalfVector2, HalfVector4, HdrBlendable) are refused, and a RenderTarget2D that asks for one silently becomes Color | all 20 XNA formats (the renderer still decides which it can render into) |
Separate alpha blending, and SourceAlphaSaturation as a destination blend factor | refused | allowed |
GraphicsDevice::GetBackBufferData (screenshots, pixel tests) | refused | allowed |
Vertex-shader texture samplers (getVertexSamplerStatesProperty()) | none: indexing the collection throws ArgumentOutOfRangeException | 4 |
| Vertex element formats | up to NormalizedShort4 | also HalfVector2, HalfVector4 |
Three limits are the same on both profiles and are not profile choices: a vertex or index buffer may not exceed 67,108,863 bytes, a vertex declaration may have at most 16 elements and 255 bytes per vertex, and a Texture2D may not be more than 2048 times longer than it is wide. Everything in the table is checked in CNA’s graphics module (GraphicsDevice, Texture2D, TextureCube, Texture3D, IndexBuffer, OcclusionQuery and Texture) and its renderer-independent defaults, at the pinned snapshot 009d40f5.
Two layers: the profile, and what the renderer can do
HiDef lifts a ceiling; it does not create a feature. There are two independent gates, and a call has to pass both:
- The profile ceiling (this page): a software rule, the same on every renderer.
- The renderer’s own capability: whether that renderer implements the feature at all, or has it on this GPU.
GraphicsDevice::SupportsCapability mixes the two inconsistently, so read it carefully. MultipleRenderTargets is answered as renderer capability AND profile limit greater than one, so it reads false under Reach even on a capable renderer. The float render-target entries are answered from the profile-aware format check, so they too read false under Reach. OcclusionQuery, by contrast, is the renderer’s answer alone: on a capable renderer it reads true under Reach, while constructing an OcclusionQuery still throws. Test the profile and the capability.
The next table is what the renderer code declares for the four HiDef-gated features that matter most, once HiDef has been requested. It is read from each renderer’s capability switch and format classifier at the pinned snapshot; it is not measured on hardware, and where a cell says “probe” the answer depends on the GPU or context at run time.
| Renderer identities | Multiple render targets | OcclusionQuery | Float / half-float render targets | Texture3D |
|---|---|---|---|---|
OPENGLES3, WEBGL2, OPENGL33 (EasyGL) | yes (up to 4) | yes; a real count on OPENGL33, only 0 or 1 on OPENGLES3 and WEBGL2 | probe | yes |
OPENGLES2, WEBGL1 (EasyGL) | no | no | no | no |
OPENGL4 | yes | yes, exact | probe | yes |
VULKAN | if the device reports more than one colour attachment | yes; exact count only with the precise-occlusion feature | probe, per format | yes |
WEBGPU | yes (2 to 4 RenderTarget2Ds) | yes, exact | probe | yes |
SDL_GPU | yes (4) | no (the vendored SDL_gpu has no query API) | probe, per format | yes |
DIRECTX11, DIRECTX12 | yes | yes | probe, per format | yes |
DIRECTX9 | not declared: the shared default answers, limited by the profile | not declared: the shared default answers; construction throws if the device lacks the query | no | not declared |
FNA3D | yes (default answer, up to 4) | yes | no (its public query is false) | yes, after a run-time read-back probe |
SOFTWARE | yes (4 CPU targets) | yes, exact | yes | yes |
METAL | no | no | no | yes |
PORTABLEGL | no (no render targets at all) | no | no | no |
HEADLESS | yes (traced, nothing is drawn) | yes (traced) | no | no |
STUB and the seven 2D-only identities (SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, FREEDIRECT, GDI, SVG_DOM) | no | no | no | no |
A game that stays inside Reach can therefore run on renderers that have none of the right-hand features (the ES2-generation GL identities, PORTABLEGL, METAL); a game that asks for HiDef and uses MRT will not. That is the real trade-off between the two profiles. For the full per-renderer picture see the renderer reference, Tutorial 101 and Tutorial 133.
Requesting HiDef
The profile has to be chosen before the device manager applies your preferences, which it does just before Initialize() (and again on ApplyChanges()). In a Game that means the constructor, exactly where XNA code sets graphics.GraphicsProfile:
MyGame::MyGame() : graphics_(this) {
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef); // before Run()
graphics_.setPreferredBackBufferWidthProperty(1280);
graphics_.setPreferredBackBufferHeightProperty(720);
}
Nothing in device creation checks whether the adapter can honour the request (in this snapshot the only renderer whose adapter answers from real hardware limits is DIRECTX9; every other renderer answers true to GraphicsAdapter::IsProfileSupported). If you want a fall-back, ask first, as CNA’s own tests do:
MyGame::MyGame() : graphics_(this) {
if (GraphicsAdapter::getDefaultAdapterProperty().IsProfileSupported(GraphicsProfile::HiDef))
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
// otherwise stay on Reach and use only the features Reach allows
}
A true from IsProfileSupported(HiDef) is not a promise that your renderer has MRT or occlusion queries; use the table above and SupportsCapability for that.
To set the profile once for the whole program rather than per manager, use the project-wide CNAEXT setting. It must run before the GraphicsDeviceManager is constructed, because the manager copies the project profile in its constructor:
#include "CNA/ProjectGraphicsProfile.hpp"
int main() {
CNA::SetProjectGraphicsProfileEXT(GraphicsProfile::HiDef); // BEFORE constructing the Game
MyGame game;
game.Run();
}
// or, as an object at namespace scope, constructed before main():
static const CNA::ProjectGraphicsProfileEXT kProfile(GraphicsProfile::HiDef);
Afterwards you can read back both what you asked for and what the device is running. Changing the manager’s profile later and calling ApplyChanges() re-applies it to the live device in CNA. In real XNA the profile is fixed when the device is created, so do not rely on that in code that has to stay XNA-portable:
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
graphics_.ApplyChanges(); // re-applies the manager's settings to the live device
GraphicsProfile wanted = graphics_.getGraphicsProfileProperty(); // what you asked for
GraphicsProfile actual = getGraphicsDeviceProperty().getGraphicsProfileProperty(); // what the device runs
What you see when something is refused
Every profile refusal below is a System::NotSupportedException unless noted. It derives from std::exception, so catch (const std::exception& e) and e.what() reach it (the messages are quoted from the source; the numbers change with the request).
| Under Reach, you | Result |
|---|---|
create a 32-bit IndexBuffer | “Thirty-two-bit index buffers are not supported by the Reach graphics profile.” |
| draw with 32-bit user indices | “Thirty-two-bit user indices are not supported by the Reach graphics profile.” |
| draw more than 65,535 primitives (or instances) at once | “primitiveCount exceeds the active graphics profile limit of 65535.” |
create a Texture2D larger than 2048 | “Texture size 4096x16 exceeds GraphicsProfile.Reach’s own maximum of 2048x2048” (a RenderTarget2D: “RenderTarget2D exceeds the active graphics profile’s maximum texture size.”) |
create a mipmapped non-power-of-two Texture2D | “Mipmapped non-power-of-two Texture2D resources are not supported by the Reach graphics profile.” |
draw with a non-power-of-two texture and LinearWrap | “Reach requires Clamp addressing for non-power-of-two Texture2D resources.” (thrown by the draw call) |
create a TextureCube of 1024 | “TextureCube: size 1024 exceeds GraphicsProfile.Reach’s own maximum cube size of 512” |
create a Texture3D | “Texture3D: GraphicsProfile.Reach does not support volume (3D) textures at all” |
create an OcclusionQuery | “OcclusionQuery is not supported by the active graphics profile and renderer.” |
| bind two render targets | “SetRenderTargets: 2 render targets exceeds GraphicsProfile.Reach’s own maximum of 1” |
set a BlendState with separate alpha factors | “Separate alpha blending is not supported by the Reach graphics profile.” |
call GetBackBufferData | “GetBackBufferData is not supported by the Reach graphics profile.” |
index getVertexSamplerStatesProperty() | System::ArgumentOutOfRangeException (index) |
ask a RenderTarget2D for a float or HDR format | no exception: it silently becomes Color. Ask first with GraphicsDevice::SupportsSurfaceFormatAsRenderTargetEXT(format) (CNAEXT), which applies the same profile rule and the renderer’s own verdict, or read getFormatProperty() afterwards. |
Once you are on HiDef, the second layer answers instead, in the renderer’s own words. A renderer that has no 32-bit index buffers reports that (GDI throws; the shared default is a std::runtime_error “32-bit index buffers are not supported by this renderer”), and a 2D-only renderer throws for every 3D call under the default Unsupported3DGraphicsCallBehavior::Throw (a std::runtime_error “<renderer> does not support 3D: <method>” on SDL_RENDERER, DIRECT2D, CANVAS and FREEDIRECT; the two DOM renderers and GDI throw their own exception types). Treat those as “this renderer cannot”, not “you forgot HiDef”.
Complete example: a profile probe
This program does one thing: it tries each of the refused operations above under the profile you choose, catches the exception, and prints a table. Run it once per profile and once per renderer you care about. The device profile is chosen exactly as shown in Requesting HiDef, with the adapter query as the guard.
// profile_probe.cpp -- what does the active GraphicsProfile allow on this renderer?
// ./profile_probe run under the default profile (Reach)
// ./profile_probe --hidef ask for HiDef first
#include <cstdio>
#include <cstring>
#include <exception>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "CNA/GraphicsCapability.hpp"
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BufferUsage.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsAdapter.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsProfile.hpp"
#include "Microsoft/Xna/Framework/Graphics/IndexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/OcclusionQuery.hpp"
#include "Microsoft/Xna/Framework/Graphics/RenderTarget2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/RenderTargetBinding.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture3D.hpp"
#include "Microsoft/Xna/Framework/Graphics/TextureCube.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
static const char* Name(GraphicsProfile p) { return p == GraphicsProfile::HiDef ? "HiDef" : "Reach"; }
class ProfileProbe final : public Game {
public:
explicit ProfileProbe(bool wantHiDef) : graphics_(this) {
graphics_.setPreferredBackBufferWidthProperty(320);
graphics_.setPreferredBackBufferHeightProperty(240);
if (wantHiDef) {
// Ask the adapter first. Only DIRECTX9 answers from real hardware caps; the other
// renderers report true, so this is not proof of any individual feature.
if (GraphicsAdapter::getDefaultAdapterProperty().IsProfileSupported(GraphicsProfile::HiDef))
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef); // applied before Initialize()
else
std::printf("HiDef is not supported by this adapter; staying on Reach\n");
}
}
protected:
void LoadContent() override {
auto& gd = getGraphicsDeviceProperty();
std::printf("profile the device runs: %s\n", Name(gd.getGraphicsProfileProperty()));
std::printf("MultipleRenderTargets : %s\n",
gd.SupportsCapability(CNA::GraphicsCapability::MultipleRenderTargets) ? "yes" : "no");
std::printf("OcclusionQuery : %s\n",
gd.SupportsCapability(CNA::GraphicsCapability::OcclusionQuery) ? "yes" : "no");
std::printf("HdrBlendable target : %s\n\n",
gd.SupportsSurfaceFormatAsRenderTargetEXT(SurfaceFormat::HdrBlendable) ? "yes" : "no");
struct Row { const char* what; std::function<std::string()> run; };
const std::vector<Row> rows = {
{ "IndexBuffer, 32-bit indices", [&] {
IndexBuffer ib(gd, IndexElementSize::ThirtyTwoBits, 6, BufferUsage::WriteOnly);
return std::string("created"); } },
{ "Texture2D 4096 x 16", [&] {
Texture2D t(gd, 4096, 16);
return std::string("created"); } },
{ "TextureCube 1024", [&] {
TextureCube c(gd, 1024, false, SurfaceFormat::Color);
return std::string("created"); } },
{ "Texture3D 16 x 16 x 16", [&] {
Texture3D v(gd, 16, 16, 16, false, SurfaceFormat::Color);
return std::string("created"); } },
{ "OcclusionQuery", [&] {
OcclusionQuery q(gd);
return std::string("created"); } },
{ "SetRenderTargets with 2 targets", [&] {
RenderTarget2D a(gd, 64, 64), b(gd, 64, 64);
try { gd.SetRenderTargets({ RenderTargetBinding(&a), RenderTargetBinding(&b) }); }
catch (...) { gd.SetRenderTarget(nullptr); throw; }
gd.SetRenderTarget(nullptr);
return std::string("bound"); } },
{ "RenderTarget2D asking for HdrBlendable", [&] {
RenderTarget2D r(gd, 64, 64, false, SurfaceFormat::HdrBlendable, DepthFormat::None);
return std::string(r.getFormatProperty() == SurfaceFormat::HdrBlendable
? "kept HdrBlendable"
: "no exception, but silently became Color"); } },
};
for (const Row& row : rows) {
try {
std::printf("[ ok ] %-40s %s\n", row.what, row.run().c_str());
} catch (const std::exception& e) {
std::printf("[FAIL] %-40s %s\n", row.what, e.what());
}
}
}
void Update(GameTime&) override { Exit(); } // one pass is enough
private:
GraphicsDeviceManager graphics_;
};
int main(int argc, char** argv) {
const bool hidef = argc > 1 && std::strcmp(argv[1], "--hidef") == 0;
ProfileProbe game(hidef);
game.Run();
return 0;
}
The project file follows Tutorial 03 (CNA and sharp-runtime cloned as siblings, both on their next branches, as in Tutorial 02):
cmake_minimum_required(VERSION 3.20)
project(ProfileProbe LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CNA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cna")
# Pick the renderer whose behaviour you want to probe. Try several: VULKAN, OPENGL4, SOFTWARE, ...
set(CNA_GRAPHICS_RENDERER "OPENGLES3" CACHE STRING "CNA graphics renderer")
set(CNA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CNA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(${CNA_DIR} ${CMAKE_BINARY_DIR}/cna)
add_executable(ProfileProbe profile_probe.cpp)
target_link_libraries(ProfileProbe PRIVATE CNA)
Build and run
cd profile_probe
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache
cmake --build build -j$(nproc)
./build/ProfileProbe # Reach, the default
./build/ProfileProbe --hidef # HiDef
On a machine with no display, wrap the run in xvfb-run -a as in Tutorial 02. What to expect follows from the source, since we did not run the program. Under Reach on a renderer that has occlusion queries (for example VULKAN, OPENGL4 or DIRECTX11), the shared device layer produces this, with the two capability lines depending on the renderer:
profile the device runs: Reach
MultipleRenderTargets : no
OcclusionQuery : yes
HdrBlendable target : no
[FAIL] IndexBuffer, 32-bit indices Thirty-two-bit index buffers are not supported by the Reach graphics profile.
[FAIL] Texture2D 4096 x 16 Texture size 4096x16 exceeds GraphicsProfile.Reach's own maximum of 2048x2048
[FAIL] TextureCube 1024 TextureCube: size 1024 exceeds GraphicsProfile.Reach's own maximum cube size of 512
[FAIL] Texture3D 16 x 16 x 16 Texture3D: GraphicsProfile.Reach does not support volume (3D) textures at all
[FAIL] OcclusionQuery OcclusionQuery is not supported by the active graphics profile and renderer.
[FAIL] SetRenderTargets with 2 targets SetRenderTargets: 2 render targets exceeds GraphicsProfile.Reach's own maximum of 1
[ ok ] RenderTarget2D asking for HdrBlendable no exception, but silently became Color
Note the two traps in that output. OcclusionQuery : yes sits above an OcclusionQuery row that fails, and the last row is a success that did not give you what you asked for. With --hidef, every row that the renderer implements changes to [ ok ], the two MRT and HDR capability lines follow the table in Two layers, and rows the renderer cannot do fail with that renderer’s own message instead (for example SDL_GPU fails the occlusion-query row, and PORTABLEGL fails the render-target rows).
Choosing between them
- Stay on Reach if your content fits: textures of 2048 or less, 16-bit indices, one render target, 8-bit colour formats, no screenshots from code. You keep the widest choice of renderers, including the ones with the fewest features.
- Ask for HiDef when you need MRT or deferred rendering, occlusion queries, cube maps larger than 512 or non-power-of-two, volume textures, float or HDR render targets (bloom, tone mapping, image-based lighting products), meshes that need 32-bit indices or more than 65,535 primitives in a call, vertex texture fetch, or
GetBackBufferDatafor pixel tests (see Tutorial 125). - Pair it with a capability check. Requesting HiDef does not select a renderer that can deliver the feature. Ask
SupportsCapabilityor the renderer capability profile and provide a fall-back.
In this tutorial series, expect to need HiDef for Tutorial 38 (32-bit indices), 61 (occlusion queries), 62 (multiple render targets), 64 (cube maps above 512), 66 (float render targets), 67 (deferred rendering), and, for large meshes, 68 and 70, and 75 for its occlusion-query section. If one of them throws a NotSupportedException that mentions the Reach graphics profile, this page is the explanation.
Limits
- We did not build or run this program. The API calls are read from the pinned snapshot’s headers and sources, and the whole file, including the fragments above, compiles (syntax only) against those headers.
- The per-renderer table is what the code declares, not a measurement, and the
DIRECTX9row is inherited defaults. Check your own device with the probe. CNA::SetProjectGraphicsProfileEXT,ProjectGraphicsProfileEXTandSupportsSurfaceFormatAsRenderTargetEXTare CNAEXT (not XNA): they are deprecated underCNA_STRICT_XNA_API.GraphicsDeviceManager::setGraphicsProfilePropertyandGraphicsAdapter::IsProfileSupportedare the XNA-shaped calls.
Key Points
- The default
GraphicsProfileis Reach, and CNA enforces it on every renderer, not justDIRECTX9as alpha.1 did. - Under Reach, MRT,
OcclusionQuery, 32-bit indices, volume textures, cubes above 512, textures above 2048, float and HDR formats, separate alpha blending andGetBackBufferDataare refused. Float render-target requests silently becomeColor. - Request HiDef before
Initialize()applies your preferences:graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef)in theGameconstructor, orCNA::SetProjectGraphicsProfileEXTbefore the manager is created. - HiDef lifts the ceiling only. Whether your renderer implements the feature is a second, separate question.
Back to Tutorial 38: Vertex Buffers and Index Buffers, or on to Tutorial 133: Read the Renderer Capability Profile.