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
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.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.Moving towards a Controller Architecture
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.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.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
@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. 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. 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(). 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.
No comments:
Post a Comment