GameWindow, GraphicsDeviceManager and the supporting types
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. Only the SDL3 platform's window failure policy was read; no test covers ApplyChanges in a constructor, the inert device-selection virtuals or a refused fullscreen request.
Around Game sit the objects a game configures and reads every frame: GameWindow, GraphicsDeviceManager and a handful of small supporting types. This page gives their exact contracts at the game-loop level — how the single window facade differs from FNA's class hierarchy and how it fails, what the manager does and does not do between construction and ApplyChanges(), which of its preferences are applied, normalized or merely recorded, and how LaunchParameters, TitleContainer, TitleLocation and FrameworkDispatcher behave. Renderer-by-renderer presentation behaviour belongs to the graphics pages; the device's side of the reset is on GraphicsDevice internals.
GameWindow: one facade over the platform window
The class comment in GameWindow.hpp states the deviation plainly: FNA defines GameWindow as abstract with per-platform subclasses; CNA keeps one concrete facade and delegates its behaviour to CNA::Platform::IPlatformWindow. The per-platform work lives behind that interface, in whichever of the seven platform implementations the build selected, so there is nothing left to subclass. GameWindow does not own the native window: the graphics device creates or adopts it during Game construction, and the game binds its window facade to it. Game, GraphicsDeviceManager and CNA::Devices::DisplayInfo are friends of the class.
The XNA surface is present: AllowUserResizing (false by default, as in XNA), ClientBounds (client-local, so the desktop position never leaks into it), CurrentOrientation, Handle, ScreenDeviceName, Title, the ClientSizeChanged, OrientationChanged and ScreenDeviceNameChanged events, BeginScreenDeviceChange / EndScreenDeviceChange and SetSupportedOrientations. The protected OnActivated, OnDeactivated and OnPaint exist with empty bodies and nothing calls them; window activation reaches the game as Game::Activated / Deactivated instead. Without a platform window (a headless configuration, or a test that builds the facade over null) the setters cache their values and the queries return the cache.
The EXT members
Names ending in EXT mark additions bolted onto an otherwise XNA-shaped class, kept visually distinct from the XNA members; most also carry the CNAEXT marker, which is what CNA_STRICT_XNA_API turns into a deprecation warning.
| Member | What it does | CNAEXT marker |
|---|---|---|
GetNativeWindowHandleEXT() | The platform-neutral native handle of the borrowed window, or an Unknown empty handle without one | yes |
MinimizeEXT(), RestoreEXT() | XNA has no minimize/restore API; no-ops without a platform window | yes |
getIsBorderlessEXTProperty(), setIsBorderlessEXTProperty() | Border visibility where the platform supports it | no — the suffix alone marks it, so strict mode does not flag it |
FileDropEXT, TextDropEXT | Drag-and-drop events; a drop's files arrive together in one FileDropEXT | yes |
SetLogicalSizeProviderEXT() | Lets orientation follow the surface the game draws rather than the window; installed by GraphicsDeviceManager | yes |
A borrowed native handle
GetNativeWindowHandleEXT() owns nothing and is valid only while the platform window lives; callers must inspect its window-system field before using a typed member. In an ordinary windowed game it describes the window the graphics device created; in a configuration without a window it is empty. Game code must never destroy the window behind it, and must not keep the handle across an explicit disposal of the graphics device, which tears the window down without notifying the facade. Older descriptions of an SDL-typed accessor on GameWindow refer to an API that no longer exists: the window contract is platform-neutral at this snapshot. The renderer-side ownership rules are on GraphicsDevice internals.
How window operations fail
GameWindow itself catches nothing: a setter propagates whatever the platform window throws. On the default SDL3 platform every failing SDL call in a setter — title, size, border, resizability, fullscreen mode, minimize, restore — is turned into a CNA::Platform::PlatformException, which derives from std::runtime_error (Sdl3Window.cpp, RequireSdlSuccess). A property setter that can throw is a real porting hazard: XNA code sets window properties freely. The deliberate exception is the client-bounds query, which a game may read every frame and which the event pump makes while it is running: on SDL3 it never throws and keeps returning the last successfully queried rectangle, covering the browser canvas's asynchronous start-up and native teardown. GraphicsDevice::UpdateViewportFromWindow applies the same policy to its surface queries, logging a warning and keeping the previous surface instead of ending the game over a resize. The other platform implementations have their own failure policies, not surveyed here.
GraphicsDeviceManager's role in the loop
GraphicsDeviceManager (GraphicsDeviceManager.cpp) implements the three-method IGraphicsDeviceManager contract — BeginDraw() returning bool, CreateDevice() and EndDraw() — and Graphics::IGraphicsDeviceService. Game::BeginDraw and Game::EndDraw delegate to it. BeginDraw() returns false when there is no device or when the renderer's CanBeginDrawEXT() says a frame cannot start (a lost browser WebGL context, for example); Game then skips Draw and EndDraw for that tick while updates continue. EndDraw() presents only if a draw was begun. The frame-lease mechanics are on One frame source trace.
Five events and their senders
The manager keeps the FNA-shaped service surface: DeviceCreated, DeviceDisposing, DeviceReset, DeviceResetting and PreparingDeviceSettings. OnDeviceCreated forwards the sender it is given; OnDeviceDisposing, OnDeviceReset and OnDeviceResetting discard it and raise with the manager as sender. The source records this asymmetry as FNA's own, preserved on purpose, and it became observable once the manager began forwarding the device's own reset events; GraphicsDeviceManagerTest.ForwardedDeviceEventsReportTheManagerAsSender pins it.
The device-selection virtuals are inert
FindBestDevice, RankDevices and CanResetDevice exist, but no manager path calls any of them. Their defaults build a default GraphicsDeviceInformation, do nothing, and answer whether a device pointer exists — where FNA's throw NotImplementedException. A subclass that overrides RankDevices to prefer a higher-resolution adapter therefore changes nothing at this snapshot; the device is the one Game constructed, and the manager only reconfigures it.
Construction and CreateDevice()
The GraphicsDeviceManager(Game*) constructor throws std::invalid_argument for a null game, points itself at the game's existing device, installs the logical-size provider on the window, registers itself under IGraphicsDeviceManager and IGraphicsDeviceService (a second manager on the same game throws) and subscribes a ClientSizeChanged handler that only refreshes the viewport — never ApplyChanges(), which would feed the physical window size in as the virtual resolution. It deliberately does not apply its preferences. Game::DoInitialize() calls CreateDevice(), the single initial configuration pass, which in order:
- builds a
GraphicsDeviceInformationfrom the preferences (size, swapped for portrait only on iOS and Android; back-buffer and depth formats; fullscreen;PresentInterval::OneorImmediatefrom the vsync Boolean; a multisample count of 0, or 8 when multisampling is preferred; the profile) and raisesPreparingDeviceSettings, whose handlers may still change it; - forwards supported orientations and brackets the window with
BeginScreenDeviceChange/EndScreenDeviceChange, which sizes the window and applies the fullscreen request; - applies the settings to the existing device: graphics profile, then presentation mode, then an in-place
GraphicsDevice::Reset, then a viewport refresh; - only after that settle-in reset subscribes to the device's own reset events, and raises
DeviceCreated.
Start-up therefore reports DeviceCreated rather than a synthetic resetting/reset pair; a later ApplyChanges() reset makes the device raise its pair, and the manager forwards each event exactly once (GraphicsDeviceManagerTest.ApplyChangesRaisesResettingAndResetExactlyOnce).
ApplyChanges(): when it runs and when it does nothing
ApplyChanges() calls CreateDevice() if the manager has no device; otherwise it returns immediately unless a preference changed since the last pass. Every preference setter, including setPreferredPresentationModeProperty and setSupportedOrientationsProperty, marks the preferences changed, and the flag starts set. When it does run it repeats the CreateDevice() pipeline from a clone of the current presentation parameters. ToggleFullScreen() is a flip of the property followed by ApplyChanges().
Where the call sits matters, because the manager has a device from the moment it is constructed:
- In the derived constructor (a common XNA and FNA habit) it performs the whole reconfiguration immediately — before the manager subscribes to the device's reset events, so no manager reset events are raised — and
CreateDevice()then does it all again duringDoInitialize. The manager's constructor comment names this double reconfiguration as a visible start-up flicker and quotes FNA's own advice not to callApplyChangesin the constructor. Set preferences there and letCreateDevice()apply them. - In
Initialize()it is a no-op unless something changed afterCreateDevice()cleared the flag. - During play (from
Update, after a settings menu) it is the intended use.
PresentationMode (CNA extension)
Declared next to the manager as CNAEXT enum class PresentationMode, with five values — Letterbox, Overscan, Stretch, NativeBackBuffer, FixedHeightDynamicWidth — it expresses what the game wants when the virtual back buffer and the actual window or display differ in aspect ratio, and it is reached through getPreferredPresentationModeProperty() / setPreferredPresentationModeProperty(). XNA never needed it on the Xbox 360 and desktop windows of its time; CNA targets browser canvases, rotating phones and arbitrary desktop windows, so it is a genuinely new subsystem, clearly marked rather than folded into the XNA-facing API. The default is Letterbox, with a source comment explaining why: XNA and FNA both report a Viewport equal to the back buffer and scale it onto whatever shape the window has, while FixedHeightDynamicWidth widens the logical width to the window's aspect and would move anything a game anchors to Viewport.Width. (Descriptions that give FixedHeightDynamicWidth as the default are older than this snapshot.) The manager applies the mode before the reset, because the renderer's logical-presentation size depends on it. The enum is a request, not a portable promise that every renderer has a matching scale, crop or letterbox pass; the renderer-side handling is on renderer backends internals.
LetterboxGame() : graphics_(this)
{
graphics_.setPreferredBackBufferWidthProperty(1280);
graphics_.setPreferredBackBufferHeightProperty(720);
graphics_.setPreferredPresentationModeProperty(PresentationMode::Letterbox); // CNAEXT
}
On a renderer that implements Letterbox, a player who resizes the window to a wider or taller shape sees bars rather than a stretched or cropped picture, and every draw call stays in the 1280 × 720 logical space.
Format and fullscreen preferences: three kinds of truth
The manager's back-buffer format starts as SurfaceFormat::Color, its depth format as DepthFormat::Depth24 and fullscreen as false; a bare PresentationParameters object, by contrast, starts with a depth format of None. All three preferences are copied into the presentation parameters of the reset, as size and interval are. After ApplyChanges() they do not have one uniform meaning:
- The manager's getters keep reporting what was requested; they are not observations.
- The device's
PresentationParametersreport what the renderer says it applied:GraphicsDevice::Resetnormalizes the requested back-buffer and depth formats through the renderer'sGetAppliedBackBufferFormatEXT/GetAppliedDepthStencilFormatEXT(identity by default) and then passes the formats and the fullscreen flag toUpdatePresentationFormatEXT. That hook is a no-op in the renderer interface; at this snapshot the DirectX 9, DirectX 11, DirectX 12, Direct2D, SDL GPU, EasyGL, OpenGL 4, FNA3D and Software families override it (and GDI overrides it with an empty body). What each family can honour is renderer-specific. - Fullscreen is a window operation. The request is applied twice per pass — by
EndScreenDeviceChangeand again by the device's reset — through the platform window. On SDL3 an exclusive request first tries the closest display mode and falls back to the desktop mode; a refusal throwsPlatformException, which the device's reset answers by restoring its previous presentation state before rethrowing. In a browser the request is deferred until a user gesture, and a still-pending request counts as success. The DirectX 11 and DirectX 12 renderers contain no call to DXGI'sSetFullscreenState, so on those renderers fullscreen is the platform window's state, never DXGI exclusive mode. Atruefullscreen property is therefore requested state, not proof that the window manager or browser accepted it. (A source comment inapplyPresentationParametersToWindowstill calls fullscreen failure non-fatal; neither that method norEndScreenDeviceChangecatches it at this snapshot.)
Vertical sync, the fixed timestep and PresentInterval::Two are covered on GameTime and the timestep.
Phones reshape the preferred back buffer; desktops do not
GraphicsDeviceManager decides once, when it is constructed, whether the process runs on a platform that cares about device orientation (GraphicsDeviceManager.cpp, platformSupportsOrientations). It compares the name that the current platform's system-information service reports with "iOS" and "Android". This is a run-time string comparison, not an #ifdef, and it copies FNA's SupportsOrientations. The ambient platform is asked, not the game's own platform, because the default constructor has no game to ask. Only the SDL3 implementation serves those two targets, and its system information reports SDL_GetPlatform().
The answer changes how CreateDevice() and ApplyChanges() turn the preferences into presentation parameters:
- Desktop, web and every other name. The back buffer is
PreferredBackBufferWidth×PreferredBackBufferHeight, taken verbatim with no landscape swap. This is XNA's and FNA's desktop behaviour.SupportedOrientationsis stored, but it is not forwarded to the window. - iOS and Android. The two preferred sizes are treated as a pair of edges. When the presentation parameters'
DisplayOrientationisPortrait, the smaller edge becomes the width. Any other value, including the default, gives the landscape shape: the larger edge becomes the width.SupportedOrientationsis passed to the window throughSetSupportedOrientationsbefore the screen-device change.
A size recorded after the user resized the window takes precedence over both rules for the next pass. In practice, a phone game that asks for 480×800 gets an 800×480 back buffer unless it also asks for Portrait. A desktop build of the same code gets 480×800.
Evidence. DefaultBackBufferPreferencesAreTheXnaDefaults covers the desktop half (GraphicsDeviceManagerPlatformTests.cpp). The test skips itself on iOS and Android, because the swap is intended there. No test exercises the phone reshape itself. Checked by reading at 009d40f5; not executed on a phone.
The supporting cast
LaunchParameters
LaunchParameters.hpp publicly inherits std::unordered_map<std::string, std::string> rather than wrapping one, so an instance is a standard associative container as well as the XNA type — and, like every standard container, it has no virtual destructor, so it must not be deleted through a base-class pointer. Game default-constructs its member, which reads the process command line: on Windows through CommandLineToArgvW converted to UTF-8, on Linux and Android from /proc/self/cmdline, and on every other platform (macOS, iOS, the web) not at all, so the game's parameters are empty there unless code fills them (getLaunchParametersProperty() returns a mutable reference, and a CNAEXT constructor takes an explicit argument vector). Parsing follows FNA: leading / and - are stripped, an argument needs at least three characters and a : that is neither first nor last, the key is everything before the first colon, and the first occurrence of a key wins; the program path itself is parsed like any other argument — a POSIX path has no colon and is skipped, but a Windows path that starts with a drive letter (C:\Games\…) yields a spurious key C. Add uses emplace and so silently ignores a duplicate key where FNA's dictionary throws. LaunchParametersTests.cpp has twelve cases for these rules.
TitleContainer and TitleLocation
TitleContainer is static-only. OpenStream(name) returns a std::unique_ptr<System::IO::Stream> opened read-only; it normalizes backslashes, resolves a relative name against TitleLocation with XNA's case-insensitive path matching, logs two CNA::Logger::Info lines per call, falls back on Android to the platform's asset loader, and throws std::runtime_error when nothing can be opened. ReadToPointer returns a malloc-allocated buffer that FreePointer releases and can throw std::bad_alloc. TitleLocation resolves the base path lazily from the platform's file-system service, falling back to the current directory as UTF-8, and exposes it under two names, getPathProperty() and a bare Path() kept to match the XNA property name; the CNAEXT setPathProperty overrides it. Its cached static state is not synchronized, so the first use should not race between threads. At this snapshot ContentManager does not open assets through TitleContainer: the two are independent direct-file facilities (the C API's stream helper is the one other in-tree caller).
FrameworkDispatcher
FrameworkDispatcher is static-only and lives in the audio module. One Update() call advances dynamic sound streams, microphone buffers, MediaPlayer and its song and state notifications, and the touch panel when a touch device exists. The base Game::Update calls it after the components on every update step, and Game's constructor calls it once. Its stream list is guarded by a mutex, but each stream is updated from a snapshot with the lock released, so a BufferNeeded handler may dispose its own stream without deadlocking on the same non-recursive mutex — a thread-safety measure beyond FNA's single-threaded assumption. Details: audio engine internals.
Evidence and limits
Read at this snapshot from GameWindow.cpp, GraphicsDeviceManager.cpp, GraphicsDevice.cpp (Reset, applyPresentationParametersToWindow, UpdateViewportFromWindow), the renderer headers under modules/renderers, LaunchParameters.cpp, TitleContainer.cpp, TitleLocation.cpp and FrameworkDispatcher.cpp. GraphicsDeviceManagerTests.cpp (8 cases), GameWindowTests.cpp, LaunchParametersTests.cpp, TitleContainerTests.cpp and TitleLocationTests.cpp exist and were not executed; window cases skip when the platform cannot create a window. Only the SDL3 platform's window failure policy was read; X11, Wayland, Win32, Headless and Terminal were not surveyed for this page. No test covers ApplyChanges() in a constructor, the inert selection virtuals or a refused fullscreen request. The Letterbox constructor was syntax-checked with g++ -std=c++23 -fsyntax-only against the TARGET headers inside a complete game class; it was not built or run.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Game loop: GraphicsDeviceManager · Game loop: GameWindow · Game loop: FrameworkDispatcher::Update()
- Architecture
- Runtime lifecycle: draw and present · Platform architecture
- Internals
- Runtime module internals: objects · Startup: transition into Run · GraphicsDevice internals · Audio engine: dispatcher
- Tests and validation
- Test architecture
- Deep dives
- The Game class · GameTime and the timestep