Updating a .NET MAUI Canvas with Messaging events

Written by wdebruin | Published 2022/11/28
Tech Story Tags: .net | xamarin | maui | publish-subscribe-pattern | emulator | emulation | game-development | software-architecture

TLDRCHIP-8 was created as a “virtual platform” to run games on a variety of kit computers in the 1970s and 1980s. It was created for graphical calculators, to play Tetris in Math class. It is considered the gateway drug for developers aspiring to game emulators. The main components of my emulators are: The Mainpage contains a Canvas for the display, a hexadecimal keyboard for getting user input. A “Game loop” is in the Chip8 class, where it can execute bytecode at a given speed expressed in instructions per second. I read input, execute instructions, then redraw the screen for relevant part.via the TL;DR App

Since I was about 10 years old I have been addicted to code. I always want to try new things. Professionally I work on MAUI apps as a software architect. The kind of architect that cannot resist to code. And so I started building emulators.

Intro

Recently I found out about the existence of a platform called “CHIP-8”. This is not a real device like for instance a Gameboy. In the 1970’s and 1980’s CHIP-8 was created as a “virtual platform” to run games on a variety of kit computers. After that CHIP-8 emulators became available for graphical calculators, to play Tetris in Math class. CHIP-8 is considered the gateway drug for developers aspiring emulation development.

The main components of my emulator are:

  • .NET MAUI, it runs on Mac, Windows, iOS, Android and Tizen devices such as fridges
  • The Mainpage contains a Canvas for the display.
  • The Mainpage contains a hexadecimal keyboard for getting user input
  • A class called “Chip8.cs” contains the actual “machine”. It has components like the memory, CPU registers, stack. It also has a “Game loop”, where it can execute bytecode at a given speed expressed in instructions per second.

The problem

My display is a Canvas element, in the MainPage. The “Game loop” is in the Chip8 class. How can I let the Mainpage know that the canvas should be redrawn?

First, the Canvas, defined in Mainpage.xaml

<GraphicsView
    x:Name="gView"
    WidthRequest="640"
    HeightRequest="320"                
    Drawable="{StaticResource GraphicsDrawable}">
</GraphicsView>

Then the code to draw. It draws pixels in a single color. Every pixel can be on or off, the resolution is 64x32. I use a PixelSize to scale pixels to a visible size on a modern display.

public class GraphicsDrawable : IDrawable
{
    public void Draw(ICanvas canvas, RectF dirtyRect)
    {
        for (int y = 0; y < Display.Pixels.GetLength(1); y++)
        {
            for (int x = 0; x < Display.Pixels.GetLength(0); x++)
            {
                var px = Display.Pixels[x, y];
                if (px == true)
                {
                    canvas.FillColor = new Color(2, 91, 24);
                }
                else
                {
                    canvas.FillColor = new Color(0, 0, 0);
                }
                canvas.FillRectangle(x * Display.PixelSize, y * Display.PixelSize, Display.PixelSize, Display.PixelSize);
            }
        }       
    }
}

public static class Display
{
    public static int PixelSize = 10; // X times 64 * 32 resolution
    public static bool[,] Pixels { get; set; } = new bool[64, 32];

    internal static void SetPixel(int x, int y, bool v)
    {
        Pixels[x, y] = v;
    }
}

Here you can see the gameloop code. At 60hz per second I read input, execute bytecode instructions, then redraw the screen. The relevant part for this article is the screen redraw.

while (true)
{
    Stopwatch t = new Stopwatch();
    t.Reset();
    t.Start();

    // Get input
    _keyPressed = CurrentKeyPressed;

    // Decrease timers at 60hz
    _regDelayTimer = (byte)(_regDelayTimer > 0x0 ? _regDelayTimer - 1 : 0x0);
    _regSoundTimer = (byte)(_regSoundTimer > 0x0 ? _regSoundTimer - 1 : 0x0);

    // Batch Execute
    await ExecuteInstructionBatch(batchSizePerHz);

    // Draw
    // How to fix this? Let MainPage know to redraw here

    t.Stop();
    await Task.Delay(Math.Max(0, 1000 / 60 - (int)t.ElapsedMilliseconds));
}

The solution

I tried to pass the canvas as a parameter to the Chip8 class, but that only got me so far. I managed to get it working on Windows, but as soon as I tried another platform I got weird rendering issues. Such as the Canvas only updating for a few frames.

Moving all the Chip8 code to the Mainpage was a working solution, but it is ugly. No seperation of concerns. And how can I maintain that monster when I start implementing other emulators?

After not touching the project for a while, I remembered that I had read about a publish/subscribe mechanism in the eBook Enterprise Application Patterns Using .NET MAUI.

This was published by Microsoft and I read it to get up to speed with the new way of doing things, me coming from Xamarin Forms. So I opened this eBook again and surely, MessagingCenter should be able to fix my problem!

In the gameloop I did:

// Draw
MessagingCenter.Send(this, "draw");

The constructor of the MainPage listens to this message and then instructs the GraphicsView of the Canvas (gView), to redraw itself.

MessagingCenter.Subscribe<Chip8>(this, "draw", (sender) =>
{
   MainThread.InvokeOnMainThreadAsync(() => gView.Invalidate());           
});

It worked like a charm!

Obsolete in .NET 7, migrate to CommunityToolkit.MVVM

Since I didn’t do much on this project for a few weeks, I did not yet migrate it from .NET 6 to .NET 7. So I went and changed the targetframeworks in the projectfiles. To my surprise Intellisense hinted me that MessagingCenter has been marked obsolete. I found this pull request from the great Gerald Versluis that added this attribute to MessagingCenter. Gerald Versluis mentions in his pull request that MessagingCenter should not be in a UI framework. Point taken, so lets look at the alternative!

IntelliSense says that I should look into the CommunityToolkit.MVVM package, it points to WeakReferenceMessenger. In the pull request there is a link to a discussion about using this instead of MessagingCenter. There it is mentioned that this messenger is 100 times faster, with a benchmark to prove it.

MVVMToolkit is cleary the fastest thing on earth. You can pick WeakReferenceMessenger or StrongReferenceMessenger. Strong is faster but has weak references, meaning less garbage collecting and manual unsubscribes are important.

To implement this, I had to add the package CommunityToolkit.MVVM to my project.

Then, I had to define a message class, inheriting from a base message class. I’m just encapsulating a string. But I can imagine I could sent the state of the display, if I want to further move components into the Chip8 class. Currently the display state is in a static class that can be accessed anywhere.

using CommunityToolkit.Mvvm.Messaging.Messages;

namespace MauiEmu;

public class DrawMessage : ValueChangedMessage<string>
{
    public DrawMessage(string value) : base(value)
    {
    }
}

Then in the gameloop, I updated the draw statement.

// Draw
StrongReferenceMessenger.Default.Send(new DrawMessage("draw"));

WeakReferenceMessenger also works. But I figured that since I only have one subscription, I don’t worry about not garbage collecting. It is a principled matter, this is faster.

In the MainPage, I update the subscription to be a StrongReferenceMessenger.

StrongReferenceMessenger.Default.Register<DrawMessage>(this, (sender, args) =>
{
    MainThread.InvokeOnMainThreadAsync(() => gView.Invalidate());
});   

After running, updating indeed seemed more frequent. It was noticeable, especially in ROMS where the screen is slowly built up, taking more then 60 frames to be loaded.

Conclusion

In .NET MAUI there is a nice way to implement a publish/subscribe pattern in your apps or games.

I will certainly keep that in mind in developing solutions, both professionally and in my hobby projects.

It is not a silver bullet, you have to keep in mind that there are other ways to communicate data between components.

Finding the best fit for each scenario is what makes software architecture so much fun!


Written by wdebruin | Dutch software development addict with .NET, Cloud and Mobile. In freetime a chess and tennis player
Published by HackerNoon on 2022/11/28