import 'dart:async'; import 'dart:isolate'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:gba_core/gba_core.dart'; import 'emulator_worker.dart'; /// A running game, seen from the UI: the emulator itself runs in its own /// isolate so that the UI stays smooth. /// /// Frames arrive as [ui.Image]s in [frame]. If the UI falls behind, frames /// in between are dropped rather than queued. final class EmulatorSession { EmulatorSession._(this._isolate, this._fromWorker); final Isolate _isolate; final ReceivePort _fromWorker; SendPort? _commands; /// The latest frame, 240×160. final ValueNotifier frame = ValueNotifier(null); /// Frames per second the emulator is managing (about 59.7 at full speed). final ValueNotifier fps = ValueNotifier(0); /// The game's title, once it has loaded. String title = ''; /// The game's save type, once it has loaded. SaveType saveType = SaveType.none; /// Whether the built-in BIOS stand-in is in use. bool usesHleBios = true; /// An error the emulator reported, if any. final ValueNotifier error = ValueNotifier(null); final Completer _stopped = Completer(); Uint8List? _pendingPixels; bool _decoding = false; bool _disposed = false; /// Starts the game at [romPath]. Completes once it is running, or throws /// a [StateError] with a message for the user if it cannot start. static Future start({ required String romPath, required String savePath, String? biosPath, }) async { final fromWorker = ReceivePort(); final ready = Completer(); final isolate = await Isolate.spawn( emulatorWorkerMain, WorkerConfig( toUi: fromWorker.sendPort, romPath: romPath, savePath: savePath, biosPath: biosPath, ), debugName: 'emulator', errorsAreFatal: true, onExit: fromWorker.sendPort, ); final session = EmulatorSession._(isolate, fromWorker); fromWorker.listen((message) => session._onMessage(message, ready)); await ready.future; final startError = session.error.value; if (session._commands == null) { throw StateError(startError ?? 'The emulator stopped while starting.'); } return session; } /// Sets the held buttons: a mask of [GbaKey] bits. void setKeys(int keys) => _commands?.send((keys: keys)); void pause() => _commands?.send(WorkerCommand.pause); void resume() => _commands?.send(WorkerCommand.resume); /// Stops the game, waiting until its save has been stored. Future stop() async { _commands?.send(WorkerCommand.stop); await _stopped.future.timeout( const Duration(seconds: 5), onTimeout: _isolate.kill, ); _dispose(); } void _onMessage(Object? message, Completer ready) { switch (message) { case ( commands: final SendPort commands, title: final String title, saveType: final SaveType saveType, usesHleBios: final bool usesHleBios, ): _commands = commands; this.title = title; this.saveType = saveType; this.usesHleBios = usesHleBios; if (!ready.isCompleted) ready.complete(); case (pixels: final TransferableTypedData pixels, fps: final double fps): this.fps.value = fps; _showFrame(pixels.materialize().asUint8List()); case (error: final String error): this.error.value = error; case stoppedMessage || null: // null: the isolate exited (onExit). if (!ready.isCompleted) ready.complete(); if (!_stopped.isCompleted) _stopped.complete(); _fromWorker.close(); } } void _showFrame(Uint8List pixels) { if (_decoding) { _pendingPixels = pixels; // Keep only the newest. return; } _decoding = true; ui.decodeImageFromPixels( pixels, Ppu.width, Ppu.height, ui.PixelFormat.rgba8888, (image) { _decoding = false; if (_disposed) { image.dispose(); return; } final previous = frame.value; frame.value = image; previous?.dispose(); final pending = _pendingPixels; if (pending != null) { _pendingPixels = null; _showFrame(pending); } }, ); } void _dispose() { if (_disposed) return; _disposed = true; frame.value?.dispose(); frame.dispose(); fps.dispose(); error.dispose(); } }