MIT License NuGet

Home / How it compares / Migrating from MonoGame

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.

The short answer

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.

  • The drawing call sites keep their XNA shape: Draw has 7 overload groups, DrawString 6 and MeasureString 2, and the value types keep their familiar members.
  • The host does not. There is no Game class, no GraphicsDeviceManager and no fixed timestep — your app is a normal Season BaseApp, and SeasonXNA plugs into its Draw2D callback.
  • Loading does not either. ContentManager and .xnb are gone; SeasonResources.LoadTexture and LoadFont replace them, preload-only.
Source level, not binary level

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.

What the compatibility layer covers

The entire public surface is 14 types across four namespaces. That number is checked mechanically, not by inspection — see the verification section below.

NamespaceTypes
Microsoft.Xna.FrameworkVector2, Rectangle, Color, Matrix
Microsoft.Xna.Framework.GraphicsTexture2D, SpriteFont, SpriteBatch, SpriteEffects, SpriteSortMode, BlendState, SamplerState
SeasonXNA.HostingDrawContext, FrameScope
SeasonXNA.InteropSeasonResources
  • 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.

Name collisions are expected

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 migration order

The recommended sequence is the one the compatibility layer was built against. The expensive mistakes are all ordering mistakes.

  1. Keep the old MonoGame build read-only, and keep a reference build of its real visuals and interaction for comparison. Start with one page or one small scene.
  2. Create a Season BaseApp host with explicit Create, Update and Draw2D boundaries. Do not implement a second Game.Run.
  3. In the loading phase, create images and fonts through SeasonXNA, and keep your own resource table, keys and path resolution.
  4. Give the host one DrawContext and one SpriteBatch, and bind them only inside the Draw2D callback Season provides.
  5. Keep the supported XNA drawing call sites; concentrate changes in constructors, 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.
  6. Replace non-drawing platform features — input, audio, screenshots, pixel processing — with native Season services. Removing MonoGame is not deleting business code.
  7. Compare page by page: image, input, saves and gameplay. Passing SeasonXNA's own acceptance suite does not mean the game is migrated.

The Content pipeline is gone

There is no content pipeline, and there will not be an emulated one. The practical consequences for a port:

  • No MGCB, no .xnb. The content builder, compiled Content artifacts and ContentManager.Load<T> are not supported and never will be. SeasonXNA does not read XNB files.
  • Assets stay in their source formats. Images are straight-alpha PNG or JPEG; fonts are TrueType or OpenType (.ttf / .otf). Text is laid out from fonts at runtime with MSDF; sprite fonts baked into XNB do not exist here.
  • Loading is explicit and preload-only. Call 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.
  • Paths belong to your app. Deployment layout (Android assets, Web wwwroot, desktop content folders) and the resource table are application concerns.
  • The web target loads asynchronously. Use 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.

What is not supported

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.

Absent at compile time

  • Windowing, the game loop, input and audio — these stay in Season's native services and platform hosts.
  • GraphicsDevice, GraphicsDeviceManager, ContentManager and the rest of the non-core XNA surface — not published.
  • Effects and shaders, render targets, screenshots and pixel readback — native Season APIs, not XNA-shaped ones.
  • Samplers beyond 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.

Rejected at runtime

InputResult
Any SpriteSortMode other than DeferredNotSupportedException
DrawString inside a PointClamp batchNotSupportedException — MSDF text requires LinearClamp
Tint with R, G or B greater than ANotSupportedException — see the tint rule below
Matrix with perspective or Z couplingNotSupportedException — planar affine only
layerDepth outside [0, 1]ArgumentOutOfRangeException — Deferred does not sort by it
Non-finite position, origin, scale, rotation, depth or matrixArgumentOutOfRangeException
Negative destination rectangle sizeArgumentOutOfRangeException — mirroring is done with scale and effects
Source rectangle with zero/negative size, or out of boundsArgumentOutOfRangeException
Unknown SpriteEffects bitsArgumentOutOfRangeException
Draw or End without Begin; Begin without EndInvalidOperationException
Missing glyph without DefaultCharacter; control characters; invalid UTF-16ArgumentException
Any use after DisposeObjectDisposedException

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.

Straight alpha and the tint rule

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.

What has been verified

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.

CheckRecorded result
Public type and assembly surface9 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 contracts12 groups, 655 assertions cumulative: binding, lifetimes, ownership, caches, exception recovery, draw order, the shared cross-platform scenes and the asynchronous loading contract.
Windows GPU acceptanceReal 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 validationThe 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:

PlatformBackendStatus
WindowsDirect3D 12Reference platform: CPU contracts, GPU images/text/animation, resize and all lifecycle paths pass.
LinuxVulkanVerified in WSL with the software llvmpipe driver and validation enabled; not a hardware-GPU performance claim.
AndroidVulkanHost lifecycle wired and compiling; on-device verification deferred.
iOS / Mac CatalystMetalManaged compilation and CPU contracts pass; no native link, MSL compilation or GPU evidence yet.
WebWebGPUCore 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.

Where this stops

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.