Home / How it compares / Migrating from MonoGame
SeasonXNA is the compatibility layer that lets XNA-shaped 2D code — sprite draws, images, MSDF text — run on SeasonEngine's renderer. This page is the migration guide: what carries over, what has to change, and what is deliberately missing.
If your MonoGame code is mostly SpriteBatch.Draw,
DrawString, Texture2D,
SpriteFont and Content.Load calls,
the shape of the drawing code survives. What does not survive is the assembly: SeasonXNA
publishes 14 public types under the original Microsoft.Xna.Framework
namespaces, and none of them come from MonoGame.
Draw has 7 overload groups, DrawString 6 and MeasureString 2, and the value types keep their familiar members.Game class, no GraphicsDeviceManager and no fixed timestep — your app is a normal Season BaseApp, and SeasonXNA plugs into its Draw2D callback.ContentManager and .xnb are gone; SeasonResources.LoadTexture and LoadFont replace them, preload-only.
Existing class libraries can be reused at source level, but not at binary level, and
only for the supported subset. A compiled library that references
MonoGame.dll cannot be swapped in, and SeasonXNA must
not be mixed with MonoGame inside the same process. Migration means recompiling your
code against SeasonXNA.
The entire public surface is 14 types across four namespaces. That number is checked mechanically, not by inspection — see the verification section below.
| Namespace | Types |
|---|---|
Microsoft.Xna.Framework | Vector2, Rectangle, Color, Matrix |
Microsoft.Xna.Framework.Graphics | Texture2D, SpriteFont, SpriteBatch, SpriteEffects, SpriteSortMode, BlendState, SamplerState |
SeasonXNA.Hosting | DrawContext, FrameScope |
SeasonXNA.Interop | SeasonResources |
Texture2D and SpriteFont have no public constructors. Handles come from SeasonResources — LoadTexture, LoadFont, BorrowTexture, BorrowFont, TakeTextureOwnership — so ownership stays explicit.SpriteBatch has a single constructor taking a DrawContext, and Begin takes only sortMode, blendState, samplerState and transformMatrix. The XNA Effect, DepthStencilState and RasterizerState parameters are not accepted-and-ignored placeholders: they do not exist.DrawContext and FrameScope replace the frame lifecycle. Bind once per Draw2D callback with using, and never keep the scope across await, threads or frames.A host that draws one image and one line of text is this shape, in full:
// XNA-shaped names at the host boundary.
using SpriteBatch = Microsoft.Xna.Framework.Graphics.SpriteBatch;
using Texture2D = Microsoft.Xna.Framework.Graphics.Texture2D;
using SpriteFont = Microsoft.Xna.Framework.Graphics.SpriteFont;
using XnaColor = Microsoft.Xna.Framework.Color;
using XnaVector2 = Microsoft.Xna.Framework.Vector2;
using XnaRectangle = Microsoft.Xna.Framework.Rectangle;
public sealed class MyApp : BaseApp
{
private readonly DrawContext _drawing = new();
private readonly SpriteBatch _sprites;
private Texture2D? _texture;
private SpriteFont? _font;
public MyApp() => _sprites = new SpriteBatch(_drawing);
// Loading phase: preload everything once, before the first frame.
public override void Create()
{
base.Create();
_texture = SeasonResources.LoadTexture("Content/Textures/Logo.png");
_font = SeasonResources.LoadFont("Content/Fonts/GameFont.ttf", 24);
}
// Recording phase: this callback only records commands into the native canvas.
public override void Draw2D(Draw2D canvas)
{
if (_texture is null || _font is null) return;
using var frame = _drawing.Bind(canvas);
try
{
_sprites.Begin(); // LinearClamp by default; required for MSDF text
_sprites.Draw(_texture, new XnaRectangle(24, 24, 320, 180), XnaColor.White);
_sprites.DrawString(_font, "Hello from SeasonXNA", new XnaVector2(24, 224), XnaColor.White);
_sprites.End();
}
catch
{
_drawing.CancelPendingBatches();
throw;
}
}
}
Install it with dotnet add package SeasonXNA; the
SeasonEngine core restores automatically as a package dependency.
MAUI and engine types such as Color,
Rectangle and Vector2
exist alongside the XNA ones. Use explicit aliases at the host boundary —
using XnaColor = Microsoft.Xna.Framework.Color; —
instead of changing engine-global usings.
The recommended sequence is the one the compatibility layer was built against. The expensive mistakes are all ordering mistakes.
BaseApp host with explicit Create, Update and Draw2D boundaries. Do not implement a second Game.Run.DrawContext and one SpriteBatch, and bind them only inside the Draw2D callback Season provides.Begin, and resource and font wrappers. Do not silence the compiler with empty API stubs — unsupported types are absent on purpose, so every affected call site is found and replaced.There is no content pipeline, and there will not be an emulated one. The practical consequences for a port:
.xnb. The content builder, compiled Content artifacts and ContentManager.Load<T> are not supported and never will be. SeasonXNA does not read XNB files..ttf / .otf). Text is laid out from fonts at runtime with MSDF; sprite fonts baked into XNB do not exist here.SeasonResources.LoadTexture(path) and LoadFont(path, size) in the loading phase. SeasonXNA never scans directories, never maintains a content manifest, and never loads during Draw.wwwroot, desktop content folders) and the resource table are application concerns.Image2D.LoadAsync and Font.CreateAsync inside the host's preload callback before the frame loop starts; the synchronous path only serves already-prepared cache entries.
The practical order for assets: delete the MGCB build step, keep the original PNG and TTF
files, port the path table, and replace every Content.Load
call with a preload call.
One design rule is stated in the code and holds everywhere: unsupported capabilities are absent at compile time or rejected at runtime. Nothing accepts a parameter and silently does nothing.
GraphicsDevice, GraphicsDeviceManager, ContentManager and the rest of the non-core XNA surface — not published.LinearClamp and PointClamp, and any BlendState beyond the AlphaBlend marker — which is a marker for the D01-A tint adaptation, not a mutable GPU blend state.| Input | Result |
|---|---|
Any SpriteSortMode other than Deferred | NotSupportedException |
DrawString inside a PointClamp batch | NotSupportedException — MSDF text requires LinearClamp |
| Tint with R, G or B greater than A | NotSupportedException — see the tint rule below |
Matrix with perspective or Z coupling | NotSupportedException — planar affine only |
layerDepth outside [0, 1] | ArgumentOutOfRangeException — Deferred does not sort by it |
| Non-finite position, origin, scale, rotation, depth or matrix | ArgumentOutOfRangeException |
| Negative destination rectangle size | ArgumentOutOfRangeException — mirroring is done with scale and effects |
| Source rectangle with zero/negative size, or out of bounds | ArgumentOutOfRangeException |
Unknown SpriteEffects bits | ArgumentOutOfRangeException |
Draw or End without Begin; Begin without End | InvalidOperationException |
Missing glyph without DefaultCharacter; control characters; invalid UTF-16 | ArgumentException |
Any use after Dispose | ObjectDisposedException |
Deliberately different semantics are documented as differences, not bugs: blend output is
not full XNA AlphaBlend target-alpha accumulation; text has
no kerning, shaping, bidi or automatic wrapping; sampling is Clamp-only with a half-texel
inset.
SeasonXNA feeds Season's straight-alpha pipeline, and two rules follow. Textures must be straight alpha: SeasonXNA never detects, unpremultiplies or repairs premultiplied images. If the old project premultiplied textures at load time, delete that pass.
Tints entering a draw must be bounded premultiplied values:
R <= A, G <= A, B <= A (reject otherwise)
A = 0 -> only RGBA all-zero is accepted (straight transparent black)
A > 0 -> converted to (R/A, G/A, B/A, A/255)
Color itself remains a plain RGBA value and can represent
anything, including RGB greater than A. But new Color(255, 0, 0, 128)
is not a legal tint; for a semi-transparent red write
Color.Red * 0.5f or
Color.FromNonPremultiplied(255, 0, 0, 128).
The two forms may differ by byte quantization — intent is never guessed.
Color * float scales all four channels (XNA behavior) and
is not changed to alpha-only scaling.
The layer is verified as a bounded contract, not by "the game compiles". The tool suite lives in the development workspace and is not shipped in the package; the numbers below are its recorded results.
| Check | Recorded result |
|---|---|
| Public type and assembly surface | 9 groups, 120,742 assertions: exactly 14 public types, Draw 7 / DrawString 6 / MeasureString 2 overload groups, a single SpriteBatch constructor, no public Texture2D or SpriteFont constructors, and no MonoGame references in the shipped assembly. |
| CPU contracts | 12 groups, 655 assertions cumulative: binding, lifetimes, ownership, caches, exception recovery, draw order, the shared cross-platform scenes and the asynchronous loading contract. |
| Windows GPU acceptance | Real screenshots with 54 RGB checks, four groups of Chinese text (per-channel tolerance 4/255, measured maximum difference 0), resize, and five lifecycle paths. |
| Linux under validation | The shared Vulkan backend in WSL with the software llvmpipe driver and Khronos validation enabled: a multi-page glyph atlas (1,800 CJK glyphs across 2 pages), transient resource release, lifecycle and empty-lease checks. |
| Web (WebGPU) | 54 RGB checks and four text comparisons (maximum difference 0) in a browser, plus device-loss and resource-count checks. The large-CJK-font stress path does not pass yet. |
Platform status, stated with the same honesty as the rest of this site:
| Platform | Backend | Status |
|---|---|---|
| Windows | Direct3D 12 | Reference platform: CPU contracts, GPU images/text/animation, resize and all lifecycle paths pass. |
| Linux | Vulkan | Verified in WSL with the software llvmpipe driver and validation enabled; not a hardware-GPU performance claim. |
| Android | Vulkan | Host lifecycle wired and compiling; on-device verification deferred. |
| iOS / Mac Catalyst | Metal | Managed compilation and CPU contracts pass; no native link, MSL compilation or GPU evidence yet. |
| Web | WebGPU | Core images, text and lifecycle paths pass in a browser; the large-CJK-font stress path does not pass yet. |
One boundary worth repeating: screenshot-based checks validate the RGB of visible output. Original target-alpha equivalence with MonoGame is not claimed.
SeasonXNA is not a MonoGame replacement, and it does not try to be. It is a bounded compatibility layer for 2D sprite, image and text call sites — for new or actively developed code that wants Season's renderer, application shell and other layers underneath familiar XNA-shaped drawing calls.
If the goal is a faithful port of a shipped XNA title, FNA is the tool built for exactly that purpose. If the goal is a full XNA-shaped framework with a content pipeline, effects and render targets, MonoGame remains the answer. What is here is a narrow, verified bridge — and the narrowness is the point.
The full contract: every type, every rule, every rejected input, and the complete verification record.
Season's core documentation: the application shell, rendering, controls and the AI layer.
Where SeasonEngine sits against MonoGame, FNA, Unity, Unreal and the rest of the C# landscape.