Skip to content

Instantly share code, notes, and snippets.

@Pieeer1
Created November 15, 2025 22:15
Show Gist options
  • Select an option

  • Save Pieeer1/26794b73a2bf9ea7f452fd7a84ef9925 to your computer and use it in GitHub Desktop.

Select an option

Save Pieeer1/26794b73a2bf9ea7f452fd7a84ef9925 to your computer and use it in GitHub Desktop.
MonoGame Blazor Runtime Engine

MonoGame Blazor Runtime Engine

Introduction

This gist shows how to run a blazor UI engine with dependency injection, blazor UI, and the full monogame functionality sitting behind it.

Warning

This is a POC and may contain serious bugs or functionality issues, however this shows the proof of concept and how it would work.

Dependencies not Included

You will need a Components/ folder in the root with the basic hello world blazor server side functionality. You can also run any other UI framework, but the example utilized is the blazor functionality.

Additionally, you need a static files wwwroot/ with the js and css styling for the above blazor functionality.

using Xilium.CefGlue;
namespace YOURNAMESPACE;
public class ActionTask : CefTask
{
private readonly Action _action;
public ActionTask(Action action) => _action = action;
protected override void Execute() => _action();
}
using Xilium.CefGlue;
namespace YOURNAMESPACE;
public class BrowserClient : CefClient
{
private readonly CefRenderHandler _renderHandler;
public BrowserClient(CefRenderHandler renderHandler)
{
_renderHandler = renderHandler;
}
protected override CefRenderHandler GetRenderHandler()
{
return _renderHandler;
}
}
using Xilium.CefGlue;
namespace YOURNAMESPACE;
public class CefAppWrapper : CefApp
{
protected override CefRenderProcessHandler GetRenderProcessHandler()
{
return new CefAppWrapperProcessHandler();
}
}
public class CefAppWrapperProcessHandler : CefRenderProcessHandler
{
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using YOURNAMESPACE.CefWrapper;
using Xilium.CefGlue;
namespace YOURNAMESPACE;
public class MainGame : Game
{
private GraphicsDeviceManager _graphics = null!;
private SpriteBatch _spriteBatch = null!;
private MonoGameRenderHandler _renderHandler = null!;
private BrowserClient _client = null!;
private CefBrowser? _browser;
public MainGame()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
}
public async Task RunAsync()
{
await Task.Run(() => Run());
}
protected override void Initialize()
{
base.Initialize();
int width = Window.ClientBounds.Width;
int height = Window.ClientBounds.Height;
_renderHandler = new MonoGameRenderHandler(GraphicsDevice, width, height);
_client = new BrowserClient(_renderHandler);
var windowInfo = CefWindowInfo.Create();
windowInfo.SetAsWindowless(IntPtr.Zero, true);
var browserSettings = new CefBrowserSettings()
{
WindowlessFrameRate = 60,
};
//CefBrowserHost.CreateBrowser(
// windowInfo,
// _client,
// browserSettings,
// "http://google.com");
CefRuntime.PostTask(CefThreadId.UI, new ActionTask(() =>
{
_browser = CefBrowserHost.CreateBrowserSync(
windowInfo,
_client,
browserSettings,
"http://localhost:61861");
}));
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
}
private MouseState _cachedMouseState;
private KeyboardState _cachedKeyboardState;
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
{
Exit();
}
var mouseState = Mouse.GetState();
var keyboardState = Keyboard.GetState();
if (_browser != null)
{
SendMouseEvent(mouseState, keyboardState);
SendKeyEvent(keyboardState);
}
_cachedMouseState = mouseState;
_cachedKeyboardState = keyboardState;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
_spriteBatch.Begin();
if (_renderHandler?.Texture != null)
{
_spriteBatch.Draw(_renderHandler.Texture, Vector2.Zero, Color.White);
}
_spriteBatch.End();
base.Draw(gameTime);
}
private void SendMouseEvent(MouseState mouseState, KeyboardState keyboardState)
{
if (_browser == null)
{
return;
}
var host = _browser.GetHost();
CefEventFlags buttonFlags = GetModifiersFromMouseState(mouseState);
CefEventFlags keyboardFlags = GetModifiersFromKeyboardState(keyboardState);
var evt = new CefMouseEvent
{
X = mouseState.X,
Y = mouseState.Y,
Modifiers = buttonFlags | keyboardFlags
};
host.SendMouseMoveEvent(evt, false);
if (mouseState.LeftButton == ButtonState.Pressed)
{
host.SendMouseClickEvent(evt, CefMouseButtonType.Left, false, 1);
}
else if (_cachedMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released)
{
host.SendMouseClickEvent(evt, CefMouseButtonType.Left, true, 1);
}
if (mouseState.RightButton == ButtonState.Pressed)
{
host.SendMouseClickEvent(evt, CefMouseButtonType.Right, false, 1);
}
else if (_cachedMouseState.RightButton == ButtonState.Pressed && mouseState.RightButton == ButtonState.Released)
{
host.SendMouseClickEvent(evt, CefMouseButtonType.Right, true, 1);
}
if (mouseState.MiddleButton == ButtonState.Pressed)
{
host.SendMouseClickEvent(evt, CefMouseButtonType.Middle, false, 1);
}
else if (_cachedMouseState.MiddleButton == ButtonState.Pressed && mouseState.MiddleButton == ButtonState.Released)
{
host.SendMouseClickEvent(evt, CefMouseButtonType.Middle, true, 1);
}
}
private void SendKeyEvent(KeyboardState keyboardState)
{
if (_browser == null) return;
var host = _browser.GetHost();
var pressedKeys = keyboardState.GetPressedKeys();
var cachedPressedKeys = _cachedKeyboardState.GetPressedKeys();
foreach (var key in pressedKeys)
{
if (!cachedPressedKeys.Contains(key))
{
bool isSpecialKey = IsSpecialKey(key);
var keyDownEvent = new CefKeyEvent
{
EventType = CefKeyEventType.KeyDown,
WindowsKeyCode = (int)key,
Modifiers = GetModifiersFromKeyboardState(keyboardState)
};
host.SendKeyEvent(keyDownEvent);
if (!isSpecialKey)
{
char character = GetCharFromKey(key, keyboardState);
if (character != '\0')
{
var charEvent = new CefKeyEvent
{
EventType = CefKeyEventType.Char,
WindowsKeyCode = character,
Modifiers = GetModifiersFromKeyboardState(keyboardState)
};
host.SendKeyEvent(charEvent);
}
}
}
}
foreach (var key in cachedPressedKeys)
{
if (!pressedKeys.Contains(key))
{
var keyUpEvent = new CefKeyEvent
{
EventType = CefKeyEventType.KeyUp,
WindowsKeyCode = (int)key,
Modifiers = GetModifiersFromKeyboardState(keyboardState)
};
host.SendKeyEvent(keyUpEvent);
}
}
_cachedKeyboardState = keyboardState;
}
private bool IsSpecialKey(Keys key)
{
switch (key)
{
case Keys.Back:
case Keys.Tab:
case Keys.Enter:
case Keys.Escape:
case Keys.LeftShift:
case Keys.RightShift:
case Keys.LeftControl:
case Keys.RightControl:
case Keys.LeftAlt:
case Keys.RightAlt:
case Keys.Up:
case Keys.Down:
case Keys.Left:
case Keys.Right:
case Keys.Delete:
case Keys.Home:
case Keys.End:
case Keys.PageUp:
case Keys.PageDown:
return true;
default:
return false;
}
}
private char GetCharFromKey(Keys key, KeyboardState keyboardState)
{
bool shift = keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift);
if (key >= Keys.A && key <= Keys.Z)
{
char c = (char)('a' + (key - Keys.A));
if (shift) c = char.ToUpper(c);
return c;
}
if (key >= Keys.D0 && key <= Keys.D9)
{
char c = (char)('0' + (key - Keys.D0));
if (shift)
{
return key switch
{
Keys.D1 => '!',
Keys.D2 => '@',
Keys.D3 => '#',
Keys.D4 => '$',
Keys.D5 => '%',
Keys.D6 => '^',
Keys.D7 => '&',
Keys.D8 => '*',
Keys.D9 => '(',
Keys.D0 => ')',
_ => c
};
}
return c;
}
if (key == Keys.Space)
{
return ' ';
}
return '\0';
}
private CefEventFlags GetModifiersFromKeyboardState(KeyboardState keyboardState)
{
CefEventFlags modifiers = CefEventFlags.None;
if (keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift))
{
modifiers |= CefEventFlags.ShiftDown;
}
if (keyboardState.IsKeyDown(Keys.LeftControl) || keyboardState.IsKeyDown(Keys.RightControl))
{
modifiers |= CefEventFlags.ControlDown;
}
if (keyboardState.IsKeyDown(Keys.LeftAlt) || keyboardState.IsKeyDown(Keys.RightAlt))
{
modifiers |= CefEventFlags.AltDown;
}
if (keyboardState.IsKeyDown(Keys.CapsLock))
{
modifiers |= CefEventFlags.CapsLockOn;
}
return modifiers;
}
private CefEventFlags GetModifiersFromMouseState(MouseState mouseState)
{
CefEventFlags modifiers = CefEventFlags.None;
if (mouseState.LeftButton == ButtonState.Pressed)
{
modifiers |= CefEventFlags.LeftMouseButton;
}
if (mouseState.MiddleButton == ButtonState.Pressed)
{
modifiers |= CefEventFlags.MiddleMouseButton;
}
if (mouseState.RightButton == ButtonState.Pressed)
{
modifiers |= CefEventFlags.RightMouseButton;
}
return modifiers;
}
}
using Microsoft.Xna.Framework.Graphics;
using Xilium.CefGlue;
namespace YOURNAMESPACE;
public class MonoGameRenderHandler : CefRenderHandler
{
private readonly GraphicsDevice _graphicsDevice;
public Texture2D Texture { get; private set; }
private readonly int _width;
private readonly int _height;
public MonoGameRenderHandler(GraphicsDevice graphicsDevice, int width, int height)
{
_graphicsDevice = graphicsDevice;
_width = width;
_height = height;
Texture = new Texture2D(_graphicsDevice, _width, _height, false, Microsoft.Xna.Framework.Graphics.SurfaceFormat.Color);
}
protected override CefAccessibilityHandler GetAccessibilityHandler()
{
throw new NotImplementedException();
}
protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
{
screenInfo.DeviceScaleFactor = 1.0f;
screenInfo.Depth = 24;
screenInfo.DepthPerComponent = 8;
screenInfo.IsMonochrome = false;
screenInfo.Rectangle = new CefRectangle(0, 0, _width, _height);
screenInfo.AvailableRectangle = new CefRectangle(0, 0, _width, _height);
return true;
}
protected override void GetViewRect(CefBrowser browser, out CefRectangle rect)
{
rect = new CefRectangle(0, 0, _width, _height);
}
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects, nint sharedHandle)
{
throw new NotImplementedException();
}
protected override void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectedRange, CefRectangle[] characterBounds)
{
throw new NotImplementedException();
}
protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects, nint buffer, int width, int height)
{
int bytes = width * height * 4;
byte[] pixelData = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(buffer, pixelData, 0, bytes);
// CEF outputs BGRA convert to RGBA for MonoGame
for (int i = 0; i < pixelData.Length; i += 4)
{
byte b = pixelData[i + 0];
byte r = pixelData[i + 2];
pixelData[i + 0] = r;
pixelData[i + 2] = b;
}
Texture.SetData(pixelData);
}
protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
{
throw new NotImplementedException();
}
protected override void OnScrollOffsetChanged(CefBrowser browser, double x, double y)
{
throw new NotImplementedException();
}
}
using YOURNAMESPACE.CefWrapper;
using YOURNAMESPACE.Components;
using Xilium.CefGlue;
namespace YOURNAMESPACE;
public static class Program
{
public static async Task Main(string[] args)
{
var cefMainArgs = new CefMainArgs(args);
var cefApp = new CefAppWrapper();
var settings = new CefSettings()
{
NoSandbox = true,
MultiThreadedMessageLoop = true,
WindowlessRenderingEnabled = true,
CachePath = Path.Combine(Path.GetTempPath(), "CefGlue_" + Guid.NewGuid().ToString().Replace("-", null))
};
CefRuntime.Initialize(cefMainArgs, settings, cefApp, IntPtr.Zero);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
AddDependencies(builder.Services);
var app = builder.Build();
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
_ = app.RunAsync();
app.Services.GetRequiredService<MainGame>().Run();
_ = app.StopAsync();
CefRuntime.Shutdown();
}
private static void AddDependencies(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<MainGame>(new MainGame());
}
}
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CefGlue.Common" Version="120.6099.211" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*" />
</ItemGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="Icon.ico" />
<None Remove="Icon.bmp" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icon.ico">
<LogicalName>Icon.ico</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Icon.bmp">
<LogicalName>Icon.bmp</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Project>
@Pieeer1
Copy link
Author

Pieeer1 commented Nov 21, 2025

https://github.com/Pieeer1/MonoBlazor

The above repository now contains a better full example. I recommend using it, as it does not require a locally run server to operate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment