How To: Multi-Screen and Sub-Viewports

SubViewGameControl is an Avalonia control that renders a secondary region of the game world using the same GL context and game tick as the primary MonoGameControl. Use it for minimaps, secondary cameras, picture-in-picture panels, or per-screen viewports in a spanning window.

Reference files: Advanced/SampleGame.MultiScreen.cs · Advanced/MainWindow.axaml · Advanced/MainWindow.axaml.cs


SubViewGameControl

Declare it in XAML alongside the primary MonoGameControl:

<Panel>
  <mg:MonoGameControl x:Name="Host" Game="{Binding Game}" />

  <Canvas x:Name="SubViewCanvas"
          HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch">
    <mg:SubViewGameControl x:Name="SubView"
                           Width="260" Height="180" />
  </Canvas>
</Panel>

Wire it in code-behind after InitializeComponent():

_subView.HostControl = _host;          // links to the MonoGameControl's GL context
_subView.Draw        = _vm.Game.DrawSubView;  // delegate called each frame

HostControl registers the sub-view with the MonoGameControl so it receives a draw callback after each game.Tick().


DrawCallback delegate

The Draw property accepts any Action<SubViewportDrawContext>:

public void DrawSubView(SubViewportDrawContext ctx)
{
    if (_spriteBatch is null || _pixel is null) return;

    var vp = ctx.Viewport;  // Rectangle in framebuffer coordinates

    _spriteBatch.Begin();
    _spriteBatch.Draw(_pixel, new Rectangle(vp.X, vp.Y, vp.Width, vp.Height),
                      new Color(20, 20, 60));
    // ... draw minimap content ...
    _spriteBatch.End();
}

SubViewportDrawContext

Property Type Description
Viewport Rectangle The sub-view's region in framebuffer coordinates

Use ctx.Viewport to confine all drawing to the sub-view's region. The sample uses GraphicsDevice.ScissorRectangle with ScissorTestEnable = true to clip any stray draws at the GPU level:

GraphicsDevice.ScissorRectangle = ctx.Viewport;
_spriteBatch.Begin(rasterizerState: _scissorRasterizer); // ScissorTestEnable = true

Per-screen spanning

In a borderless window that spans multiple physical displays, create one SubViewGameControl per screen, sized and positioned to cover each display's portion of the canvas:

// For each active screen (sorted left-to-right):
var sv = new SubViewGameControl
{
    Width       = screenWidthInDip,
    Height      = screenHeightInDip,
    HostControl = _host,
};

// Route rendering:
sv.Draw = isPrimary
    ? ctx => game.DrawPrimaryView(ctx)      // shares main animation state
    : ctx => game.DrawScreenViewport(rank, total, ctx);  // independent per-screen

Canvas.SetLeft(sv, canvasLeft);
Canvas.SetTop(sv,  canvasTop);
_subViewCanvas.Children.Insert(rank, sv);  // insert at back to keep overlay on top

See UpdatePerScreenViews() in MainWindow.axaml.cs for the complete implementation.


SuppressMainDraw

When per-screen sub-viewport delegates own all rendering, the main Draw() pass would clear the framebuffer and overwrite content already rendered by those delegates. Set SuppressMainDraw = true on the game to skip the main draw pass:

// Enable per-screen mode:
game.SuppressMainDraw = true;

// Disable (back to single-screen):
game.SuppressMainDraw = false;

Implement the guard in the game's Draw() method:

protected override void Draw(GameTime gameTime)
{
    // FPS tracking runs regardless so the counter stays live.
    UpdateFps(gameTime);

    if (SuppressMainDraw)
    {
        GraphicsDevice.Clear(Color.Black);
        return;
    }

    // Normal single-screen draw pass:
    GraphicsDevice.Clear(new Color(8, 12, 35));
    DrawMainContent(...);
    base.Draw(gameTime);
}

DrawPrimaryView — seamless single↔multi transition

To avoid a visible discontinuity when switching between single-screen and multi-screen mode, route the primary screen's sub-viewport delegate to a method that shares the main-view animation state:

// Primary screen uses the same bouncer list as the main draw pass:
public void DrawPrimaryView(SubViewportDrawContext ctx)
{
    var dt = (float)(_currentGameTime?.ElapsedGameTime.TotalSeconds ?? 0);
    DrawMainContent(ctx.Viewport, dt, viewportFillOnly: true);
}

Non-primary screens each receive an independent animation via DrawScreenViewport.


Unregistering sub-views

Set HostControl = null to unregister a SubViewGameControl before removing it from the layout tree:

foreach (var sv in _perScreenSubViews)
    sv.HostControl = null;

_subViewCanvas.Children.RemoveAll(_perScreenSubViews);
_perScreenSubViews.Clear();
latest ▼