Tuesday, 24 March 2026

A Commodore 64 Emulator in Flutter: Part 16

Foreword

In the previous post we managed to implement Tape loading, and managed to emulate this process until it shows it found a file name.

We ended the post discovering that the emulator has a serious memory leak, which will attempt to solve in this post.

Unpacking the Memory Leak

In the previous post we saw that our emulator had a serious memory leak, where memory usage grew over 1 Gigabyte in less than half an hour.

I carefully went through my code but couldn't really find any obvious place where a memory leak was happening. 

I did, however, had a suspicion that the cause of the memory leak was probably related to how the frames was rendered to the screen. This is probably the process in the emulator where the most data move back and forth.

Eventually I debated with ChatGPT and Gemini which components are the best for doing rendering in flutter which would cause memory leaks. I tried all the suggestions but didn't really resulted in fixing the memory leak.

Eventually Gemini came with a suggestion to use a native HTML canvas to do the rendering. This strike as a sensible idea as I was using an HTML canvas in one of my JavaScript Emulators I used about 10 years, without any memory leak.

Also, I started to realise my current rendering implementing was perhaps on the heavy side. With a Bloc emitting a state change on every frame, part of the widget tree was being redrawn 60 times a second. This sounded very intense, so the idea of reusing an HTML canvas seemed like a way out.

I did a proof of concept, and create a small Flutter project, using the HTML canvas as described, and just rendered some simple, like a moving line, being redrawn 60 times a second. In this proof of concept I actually found that the memeory usage remained within bounds.

So, in this post I will be following this approach of rewriting the emulator to make use of an HTML Canvas, in order to eliminate the memory leak.

Bringing a native HTML canvas to Flutter

Let us pause for moment, and see how we can introduce a native HTML canvas in Flutter.

We begin with a simple class:
class EmulatorCanvas {
  late final html.CanvasElement canvas;
  late final html.CanvasRenderingContext2D ctx;

  final int width;
  final int height;
  int inc = 0;

  EmulatorCanvas(this.width, this.height) {
    canvas = html.CanvasElement(width: width, height: height);
    ctx = canvas.context2D;

    // Register with Flutter
    ui.platformViewRegistry.registerViewFactory(
      'emulator-canvas',
          (int viewId) => canvas,
    );
  }

}

In this code, html is a Dart package, and html.CanvasElement actually creates us a native HTML Canvas element. It is important to note that at this stage, the created canvas element is not attached to the HTML page at the moment.

The registerViewFactory actually allows the widget tree to have access to the created Canvas element, and we associate the name emulator-canvas with it.

Let us now see where we will use thus class:

import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'emulator_canvas.dart';
import 'emulator_controller.dart';

class VideoScreen extends StatefulWidget {
  const VideoScreen({super.key});

  @override
  State<VideoScreen> createState() => _VideoScreenState();
}

class _VideoScreenState extends State<VideoScreen>
    with SingleTickerProviderStateMixin {

  late EmulatorCanvas emCanvas;

  @override
  void initState() {
    super.initState();

    emCanvas = EmulatorCanvas(320, 200);

  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF4040E0),
      body: Column(
        children: [

          const SizedBox(
            width: 640,
            height: 400,
            child: HtmlElementView(viewType: 'emulator-canvas'),
          ),
        ],
      ),
    );
  }
}
Firstly we have VideoScreen as a StatefulWidget, which meand we will reuse this instance, and it will not be destroyed with every state change.

As the name implies, StatefulWidget, the Widget should contain state that should be mutable. For this reason our Widget ties to the class _VideoScreenState. Something interesting about this class is that it is declared with with SingleTickerProviderStateMixin. This means that ticker events is synchronised to screen refreshes and is called once per screen refresh.

Here we also declare an Instance of emCanvas. Finally with the widget we return via the build method, we also wrap the emCanvas instance into it with the label emulator-canvas. Previously we registered EmCanvas with flutter by that name, so in that way we can associate it inside our returning widget.

As an additional extra, it is interesting to inspect the HTML in the browser:


One can actually see the canvas element of what we defined in our code.

Obviously all this needs to be wired up all the way until the main screen in main.dart, which we will cover in the next section.

Moving towards a Controller Architecture

Up to now the centre of our C64 emulator in flutter was a BloC. In our BloC we emitted a new state with every frame, which instructed the front end to create a new widget instance for displaying the new frame.

As indicated earlier in this post, this is really clunky considering we need to render 60 frames a second. To get around this, we will discard our BloC idea and rather opt for a Controller architecture.

Let us start at the highest level, main.dart, which hosts our flutter application:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final controller = await EmulatorController.create();
  runApp(
    MaterialApp(
      home: RepositoryProvider.value(
        value: controller,
        child: const EmulatorRoot(),
      )
    ));
}
Now, as you might have guest, the core functionality of our emulator will live in the class EmulatorController. We will perform more or less the same things we performed in our old C64BloC class.

You might also remember that when we were previously writing our C64BloC class, we did some asynchronous tasks, loading the three C64 ROMS from disk, and we had to use use the keyword await to wait until everything was into memory before we continue. We need to do something similar with our controller, which necessitates us to declare our main() method as async, in order to use the await functionality.

In our main() method, we also make use of a RepositoryProvider. This enables us to inject our controller class further down in our tree where it might be needed.

To help us orientate everything let us have a look at the implementation of EmulatorRoot:

class EmulatorRoot extends StatefulWidget {
  // final String name;
  const EmulatorRoot({super.key});

  @override
  State<EmulatorRoot> createState() => _EmulatorRootState();
}

class _EmulatorRootState extends State<EmulatorRoot> {
  int _currentIndex = 0; // 0 = debug, 1 = video

  @override
  Widget build(BuildContext context) {
    EmulatorController controller = context.read<EmulatorController>();
    return Scaffold(
      appBar: AppBar(
        title: const Text("C64 Emulator"),
        actions: [
          IconButton(
            icon: const Icon(Icons.bug_report),
            onPressed: () => setState(() => _currentIndex = 0),
          ),
          IconButton(
            icon: const Icon(Icons.tv),
            onPressed: () => setState(() => _currentIndex = 1),
          ),
        ],
      ),
      body: IndexedStack(
        index: _currentIndex,
        children: [
          // DebugScreen(),
          KeyboardListener (
            // VideoScreen(),
            focusNode: controller.focusNode,
            autofocus: true,
            onKeyEvent: (event) => {
              if (event is KeyDownEvent) {
                controller.keyboardEvent(event.logicalKey, true)
                // context.read<C64Bloc>().add(KeyC64Event(keyDown: true, key: event.logicalKey))
              } else if (event is KeyUpEvent) {
                controller.keyboardEvent(event.logicalKey, false)
                // context.read<C64Bloc>().add(KeyC64Event(keyDown: false, key: event.logicalKey))
              }
            },
            child: const VideoScreen(),

          )
        ],
      ),
    );
  }
}
This is yet another StatefulWdiget with Associated state. The basic idea outlined here is to have a tabbed view, showing a debug view on one tab and the screen of the running emulator in another tab. We will not show how to implement the Debug tab in this series and is just shown as a possibility into how this emulator can develop.

For the tab showing the runnig emulator screen, we casically show VideoScreen, which we developed earlier on. We have also wrapped this screen with a KeyboarListener, for interception keystrokes. This is similar as we did in previous posts.

You will also see with the keyevents we call controller.keyboardEvent. This will interface our emulator with a keyboard.

Let us next, look at the internals of EmulatorController:

class EmulatorController implements KeyInfo{
  final Memory memory = Memory();
  final List<int> matrix = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
  late final Cpu _cpu = Cpu(memory: memory);
  late final Tape _tape;
  late final Alarms alarms = Alarms();
  FocusNode focusNode = FocusNode();
  bool tapeLoaded = false;

  EmulatorController._();

  static Future<EmulatorController> create() async {
    final instance = EmulatorController._();
    await instance._init();
    return instance;
  }

  Future<void> _init() async {
    final basicData = await rootBundle.load("assets/basic.bin");
    final characterData = await rootBundle.load("assets/characters.bin");
    final kernalData = await rootBundle.load("assets/kernal.bin");
    Cia1 cia1 = Cia1(alarms: alarms);
    cia1.setKeyInfo(this);
    Tape tape = Tape(alarms: alarms, interrupt: cia1);
    _tape = tape;
    memory.setCia1(cia1);
    memory.populateMem(basicData, characterData, kernalData);
    memory.setTape(tape);
    _cpu.setInterruptCallback(() => cia1.hasInterrupts());
    _cpu.reset();
  }
...
}
This is pretty much the same we did in our Bloc. There is, however, a couple of things we do extra. We hide the constructor and to get a new instance, we need to call create() to give us a properly initialised instance.

Implementing Screen Refreshing

So, we have just implemented the basics for a controller architecture for our Flutter C64 emulator. Let us next focus on how to render the frames.

Firstly, within video_screen.dart, we need to make this file aware of our controller within the initState() method:

  @override
  void initState() {
    super.initState();

    controller = context.read<EmulatorController>();
    emCanvas = EmulatorCanvas(320, 200);
    controller.setCanvasArray(emCanvas.getFrameBuffer());
    ...
  }
We use context.read to get the injected instance of the controller which was injected higher in the tree. Once we have the controller instance we pass it through the the framebuffer of the canvas, so that our controller do some drawing if required.

In an earlier section in this post I briefly talk about the use of SingleTickerProviderStateMixin, which syncs frame refreshes with refresh rate of the screen. We will now go further with this implementation inside the initState() method.

  void initState() {
    super.initState();

    controller = context.read<EmulatorController>();
    emCanvas = EmulatorCanvas(320, 200);
    controller.setCanvasArray(emCanvas.getFrameBuffer());

    _ticker = createTicker((Duration elapsed) {
      if ((elapsed.inMilliseconds - lastProcessed) < 16) {
        return;
      }
      lastProcessed = elapsed.inMilliseconds;

      controller.executeChunk();
      emCanvas.renderFrame();
    });

    _ticker.start();
  }

Here we create a ticker instance, which will execute with every screen refresh. With controller.executeChunk(), we tell our emulator execute one frame worth of cycles. This is more or less the same approach we implemented previously. 

You will also see that we throttle the rendering a bit to get close to the real speed of a C64, by just exiting the ticker body if it is not yet time to display the next frame. Having said that, most displays refreshes at a rate of 60Hz. So, if you take out the return code, you emulator should still run at more or less the same speed of a real C64.

Next, let us look at the implementation of controller.executeChunk():

  void executeChunk() {
    int targetCycles = _cpu.getCycles() + 16666;
    do {
      _cpu.step();
      alarms.processAlarms(_cpu.getCycles());
    } while (_cpu.getCycles() < targetCycles);
    memory.renderDisplayImage();
  }

So, in thus method we execute a frame worth of CPU cyles and we render a frame to be displayed. The Array we render to is the one we passed through earlier with controller.setCanvasArray()

Let us finally have a look at the implementation of emCanvas.renderFrame():

  void renderFrame() {
    ctx.putImageData(imageData, 0, 0);
  }

Here we are dealing raw HTML territory. WIth ctx.putImageData, we write to the actual HTML Canvas element we defined earlier.

In Summary

In this post we reworked our C64 emulator to a Controller architecture in order to fix a memory leak. With our new architecture, we don't create a new widget with every frame, but rather maintain a single HTML Canvas element throughout the life cycle of our emulator, to which we render all frames.

In the next post we will add some colors to our C64 frames, together with some borders. We will also emulate the drawing of the border in a more granular fashion, in order to accurately similate the flashing borders while loading the game from a tape image.

As usual, you can find the source for every post on my GitHub page. For this post, you can go here

Until next time! 

No comments:

Post a Comment