Strings blurness problem while using Scale Matrix Independent Resolution Rendering

My entire codebase is structured this way, but I’m facing a major issue with UI scaling.

When I use _screenScaleMatrix, both the UI and text become blurry. However, if I remove it, the UI appears too small on higher-resolution screens. Additionally, when I resize the window, the UI stretches and fills the entire screen, which is not the intended behavior.

I’ve been struggling to solve this problem for almost a week and haven’t found a proper solution yet.

// c_main

    public class Main : Game

    {

private readonly GraphicsDeviceManager _graphicsDeviceManager;

private SpriteBatch _spriteBatch;

private DebugConsole _debugConsole;

private DebugUI _debugUI;

public static int _resolutionWidth = 1280;    // desired width

public static int _resolutionHeight = 720;    // desired height

public static int _virtualWidth = 1280;    // width we render

public static int _virtualHeight = 720;    // height we render

private bool _isResizing;

private Matrix _screenScaleMatrix;

private Viewport _viewport;




// constructor f_main

public Main()

        {

            Globals.Main = this;

            _graphicsDeviceManager = new GraphicsDeviceManager(this)

            {

                PreferredBackBufferWidth = _resolutionWidth,

                PreferredBackBufferHeight = _resolutionHeight

            };

            _graphicsDeviceManager.ApplyChanges();




            Content.RootDirectory = "Content";

            IsMouseVisible = true;




            Window.Title = Config.GameWindowTitle;

            Window.AllowUserResizing = true;

            Window.ClientSizeChanged += OnClientSizeChanged;

            Globals.SceneManager = new();

        }




// initialize f_main

protected override void Initialize()

        {

            UpdateScreenScaleMatrix();




// globals

            Globals.GraphicsDevice = GraphicsDevice;

            Globals.GraphicsDeviceManager = _graphicsDeviceManager;

            Globals.ContentManager = Content;

            Globals.InputManager = new InputManager();

            Globals.AudioManager = new AudioManager();




            _debugUI = new DebugUI();




            SaveSystem.Initialize();

            SaveSystem.LoadValues();




base.Initialize();

        }




// load content f_main

protected override void LoadContent()

        {

            _spriteBatch = new SpriteBatch(GraphicsDevice);




            Globals.SpriteBatch = _spriteBatch;




if (Config.IsDebugMode)

                _debugConsole = new DebugConsole();




            Globals.MainFontS = Content.Load<SpriteFont>(AssetPath.Font.MainS);

            Globals.MainFontM = Content.Load<SpriteFont>(AssetPath.Font.MainM);

            Globals.MainFontL = Content.Load<SpriteFont>(AssetPath.Font.MainL);




            Globals.SceneManager.InitTransitions();

            Globals.SceneManager.PushScene(new GameplayScene());




            NotificationUI.Initialize();

        }




// update f_main

protected override void Update(GameTime gameTime)

        {

            Globals.InputManager.Update();

            Globals.SceneManager.Update(gameTime);

            Globals.AudioManager.Update();

            NotificationUI.Update(gameTime);

            _debugConsole?.Update();




if (Config.ShowDebugUI)

                _debugUI?.Update(gameTime);




base.Update(gameTime);

        }




// draw f_main

protected override void Draw(GameTime gameTime)

        {

            GraphicsDevice.Clear(Color.Black);

            GraphicsDevice.Viewport = _viewport;




            _spriteBatch.Begin(samplerState: SamplerState.PointClamp, transformMatrix: _screenScaleMatrix);

            Globals.SceneManager.Draw(gameTime);

            _spriteBatch.End();




            _spriteBatch.Begin(samplerState: SamplerState.PointClamp);

            NotificationUI.Draw();

            _debugConsole?.Draw();

            _debugUI?.Draw(gameTime);

            _spriteBatch.End();




base.Draw(gameTime);

        }




// unload content f_main

protected override void UnloadContent()

        {

            Globals.AudioManager.Dispose();

            Globals.ContentManager.Dispose();

            AssetManager.Dispose();




base.UnloadContent();

        }




// update screen scale matrix f_main

public void UpdateScreenScaleMatrix()

        {

// determine the size of actual screen

float screenWidth = GraphicsDevice.PresentationParameters.BackBufferWidth;

float screenHeight = GraphicsDevice.PresentationParameters.BackBufferHeight;




// calculate the virtual resolution based on the current screen resolution compared to our intended resolution

if (screenWidth / _resolutionWidth > screenHeight / _resolutionHeight)

            {

float aspect = screenHeight / _resolutionHeight;

                _virtualWidth = (int)(aspect * _resolutionWidth);

                _virtualHeight = (int)screenHeight;

            }

else

            {

float aspect = screenWidth / _resolutionWidth;

                _virtualWidth = (int)screenWidth;

                _virtualHeight = (int)(aspect * _resolutionHeight);

            }




            _screenScaleMatrix = Matrix.CreateScale(_virtualWidth / (float)_resolutionWidth);




            _viewport = new Viewport

            {

                X = (int)(screenWidth / 2 - _virtualWidth / 2),

                Y = (int)(screenHeight / 2 - _virtualHeight / 2),

                Width = _virtualWidth,

                Height = _virtualHeight,

                MinDepth = 0,

                MaxDepth = 1

            };

        }




// screen on client size changed f_main

private void OnClientSizeChanged(object sender, EventArgs e)

        {

if (!_isResizing && Window.ClientBounds.Width > 0 && Window.ClientBounds.Height > 0)

            {

                _isResizing = true;

                UpdateScreenScaleMatrix();

                _isResizing = false;

            }

        }

    }

I switched to using BitmapFont from MonoGame.Extended, and it completely solved the issue. This confirms that the problem was with SpriteFont rather than my code structure. Thanks to @takopuck from Discord for the help!