Skip to content

Commit

Permalink
Persist some data in the simulator across runs.
Browse files Browse the repository at this point in the history
The contents of the "Saved" folder that's mounted in the Mac via ExtFS
are saved in IndexedDB. This us done using a homegrown method that
extracts the contents during emulator unloading and uses the idb-keyval
wrapper to store to. Emscripten's IDBFS is not an options since it
relies on an event loop, which not an option.

Communication of the emulator stop is done via a synthetic "stop"
event so that we don't need to set up an additional communication path
for it.
  • Loading branch information
mihaip committed Nov 15, 2021
1 parent c786938 commit 0899205
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 9 deletions.
2 changes: 1 addition & 1 deletion macemu
15 changes: 14 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -14,6 +14,7 @@
"@types/react-dom": "^17.0.0",
"file-saver": "^2.0.5",
"husky": "^6.0.0",
"idb-keyval": "^6.0.3",
"jszip": "^3.7.0",
"lint-staged": "^11.0.0",
"prettier": "^2.3.1",
Expand Down
15 changes: 14 additions & 1 deletion src/BasiliskII/emulator-common.ts
Expand Up @@ -7,6 +7,7 @@ export const InputBufferAddresses = {
keyEventFlagAddr: 5,
keyCodeAddr: 6,
keyStateAddr: 7,
stopFlagAddr: 8,
};

export type EmulatorMouseEvent =
Expand All @@ -18,11 +19,15 @@ export type EmulatorKeyboardEvent = {
type: "keydown" | "keyup";
keyCode: number;
};
export type EmulatorStopEvent = {
type: "stop";
};

export type EmulatorInputEvent =
| EmulatorMouseEvent
| EmulatorTouchEvent
| EmulatorKeyboardEvent;
| EmulatorKeyboardEvent
| EmulatorStopEvent;

export enum LockStates {
READY_FOR_UI_THREAD,
Expand Down Expand Up @@ -52,6 +57,7 @@ export type EmulatorWorkerConfig = {
wasmUrl: string;
disk: EmulatorChunkedFileSpec;
autoloadFiles: {[name: string]: ArrayBuffer};
persistedData?: EmulatorWorkerDirectorExtraction;
arguments: string[];
video: EmulatorWorkerVideoConfig;
input: EmulatorWorkerInputConfig;
Expand Down Expand Up @@ -166,6 +172,7 @@ export function updateInputBufferWithEvents(
let hasKeyEvent = false;
let keyCode = -1;
let keyState = -1;
let hasStop = false;
// currently only one key event can be sent per sync
// TODO: better key handling code
const remainingEvents: EmulatorInputEvent[] = [];
Expand Down Expand Up @@ -203,6 +210,9 @@ export function updateInputBufferWithEvents(
keyState = inputEvent.type === "keydown" ? 1 : 0;
keyCode = inputEvent.keyCode;
break;
case "stop":
hasStop = true;
break;
}
}
if (hasMouseMove) {
Expand All @@ -217,6 +227,9 @@ export function updateInputBufferWithEvents(
inputBufferView[InputBufferAddresses.keyCodeAddr] = keyCode;
inputBufferView[InputBufferAddresses.keyStateAddr] = keyState;
}
if (hasStop) {
inputBufferView[InputBufferAddresses.stopFlagAddr] = 1;
}
return remainingEvents;
}
export type EmulatorWorkerDirectorExtractionEntry =
Expand Down
18 changes: 18 additions & 0 deletions src/BasiliskII/emulator-ui-persistence.ts
@@ -0,0 +1,18 @@
import type {EmulatorWorkerDirectorExtraction} from "./emulator-common";
import * as idbKeyVal from "idb-keyval";

export async function persistData(
extraction: EmulatorWorkerDirectorExtraction
) {
await idbKeyVal.set(EXTRACTION_KEY, extraction);
}

export async function getPersistedData(): Promise<
EmulatorWorkerDirectorExtraction | undefined
> {
const extraction = await idbKeyVal.get<EmulatorWorkerDirectorExtraction>(
EXTRACTION_KEY
);
return extraction;
}
const EXTRACTION_KEY = "extraction";
19 changes: 15 additions & 4 deletions src/BasiliskII/emulator-ui.ts
Expand Up @@ -2,6 +2,7 @@ import type {
EmulatorChunkedFileSpec,
EmulatorFallbackCommand,
EmulatorWorkerConfig,
EmulatorWorkerDirectorExtraction,
EmulatorWorkerVideoBlit,
} from "./emulator-common";
import Worker from "worker-loader!./emulator-worker";
Expand Down Expand Up @@ -31,6 +32,7 @@ import {
import {handleDirectoryExtraction} from "./emulator-ui-extractor";
import BasiliskIIPath from "./BasiliskII.jsz";
import BasiliskIIWasmPath from "./BasiliskII.wasmz";
import {getPersistedData, persistData} from "./emulator-ui-persistence";

export type EmulatorConfig = {
useTouchEvents: boolean;
Expand Down Expand Up @@ -169,6 +171,8 @@ export class Emulator {
}
);

const extraction = await getPersistedData();

const config: EmulatorWorkerConfig = {
jsUrl: jsBlobUrl,
wasmUrl: wasmBlobUrl,
Expand All @@ -177,6 +181,7 @@ export class Emulator {
"Quadra-650.rom": rom,
"prefs": prefs,
},
persistedData: extraction,
arguments: ["--config", "prefs"],

video: this.#video.workerConfig(),
Expand Down Expand Up @@ -213,10 +218,7 @@ export class Emulator {
this.#handleVisibilityChange
);

this.#worker.removeEventListener("message", this.#handleWorkerMessage);

this.#worker.terminate();
this.#workerTerminated = true;
this.#input.handleInput({type: "stop"});
}

uploadFile(file: File) {
Expand Down Expand Up @@ -317,6 +319,8 @@ export class Emulator {
handleDirectoryExtraction(e.data.extraction);
} else if (e.data.type === "emulator_first_idlewait") {
console.timeEnd("Emulator first idlewait");
} else if (e.data.type === "emulator_stopped") {
this.#handleEmulatorStopped(e.data.extraction);
} else {
console.warn("Unexpected postMessage event", e);
}
Expand Down Expand Up @@ -394,6 +398,13 @@ export class Emulator {
this.#handleServiceWorkerMessage
);
}

async #handleEmulatorStopped(extraction: EmulatorWorkerDirectorExtraction) {
await persistData(extraction);
this.#worker.removeEventListener("message", this.#handleWorkerMessage);
this.#worker.terminate();
this.#workerTerminated = true;
}
}

async function load(
Expand Down
10 changes: 9 additions & 1 deletion src/BasiliskII/emulator-worker-extractor.ts
Expand Up @@ -40,7 +40,9 @@ const EXTRACTOR_DIRECTORY = "/Shared/Uploads";
const extractedDirectories = new Set<string>();
const extractedFiles = new Set<string>();

function extractDirectory(dirPath: string) {
export function prepareDirectoryExtraction(
dirPath: string
): [EmulatorWorkerDirectorExtraction, ArrayBuffer[]] {
const {name: dirName, parentPath: dirParentPath} = FS.analyzePath(dirPath);
const arrayBuffers: ArrayBuffer[] = [];
const extraction: EmulatorWorkerDirectorExtraction = {
Expand Down Expand Up @@ -94,6 +96,12 @@ function extractDirectory(dirPath: string) {
});
}

return [extraction, arrayBuffers];
}

function extractDirectory(dirPath: string) {
const [extraction, arrayBuffers] = prepareDirectoryExtraction(dirPath);

self.postMessage(
{type: "emulator_extract_directory", extraction},
arrayBuffers
Expand Down
46 changes: 46 additions & 0 deletions src/BasiliskII/emulator-worker-persistence.ts
@@ -0,0 +1,46 @@
import type {
EmulatorWorkerDirectorExtraction,
EmulatorWorkerDirectorExtractionEntry,
} from "./emulator-common";

export function restorePersistedData(
dirPath: string,
extraction: EmulatorWorkerDirectorExtraction
) {
function restore(
parentDirPath: string,
entries: EmulatorWorkerDirectorExtractionEntry[]
) {
for (const entry of entries) {
if (Array.isArray(entry.contents)) {
const childDirPath = `${parentDirPath}/${entry.name}`;
FS.mkdir(childDirPath);
restore(childDirPath, entry.contents);
} else {
let entryParentDirPath = parentDirPath;
let entryName = entry.name;
if (entryName === "DInfo") {
// Write the DInfo struct for this folder in the parent's .finf
// directory.
entryName = extraction.name;
entryParentDirPath = `${
FS.analyzePath(dirPath).parentPath
}/.finf`;
if (!FS.analyzePath(entryParentDirPath).exists) {
FS.mkdir(entryParentDirPath);
}
}
FS.createDataFile(
entryParentDirPath,
entryName,
entry.contents,
true,
true,
true
);
}
}
}

restore(dirPath, extraction.contents);
}
28 changes: 27 additions & 1 deletion src/BasiliskII/emulator-worker.ts
Expand Up @@ -33,15 +33,19 @@ import {createLazyFile} from "./emulator-worker-lazy-file";
import {
handleExtractionRequests,
initializeExtractor,
prepareDirectoryExtraction,
} from "./emulator-worker-extractor";
import {
createChunkedFile,
validateSpecPrefetchChunks,
} from "./emulator-worker-chunked-file";
import {restorePersistedData} from "./emulator-worker-persistence";

declare const Module: EmscriptenModule;
declare const workerCommands: EmulatorFallbackCommand[];

const PERSISTED_DIRECTORY_PATH = "/Shared/Saved";

self.onmessage = function (event) {
const {data} = event;
const {type} = data;
Expand All @@ -64,6 +68,7 @@ class EmulatorWorkerApi {
#lastIdleWaitFrameId = 0;

#gotFirstIdleWait = false;
#handledStop = false;
#diskSpec: EmulatorChunkedFileSpec;

constructor(config: EmulatorWorkerConfig) {
Expand Down Expand Up @@ -177,6 +182,10 @@ class EmulatorWorkerApi {
// when the machine is idle seems reasonable.
this.#handleFileUploads();
handleExtractionRequests();

if (this.getInputValue(InputBufferAddresses.stopFlagAddr)) {
this.#handleStop();
}
}

#handleFileUploads() {
Expand All @@ -193,6 +202,17 @@ class EmulatorWorkerApi {
}
}

#handleStop() {
if (this.#handledStop) {
return;
}
this.#handledStop = true;
const [extraction, arrayBuffers] = prepareDirectoryExtraction(
PERSISTED_DIRECTORY_PATH
);
postMessage({type: "emulator_stopped", extraction}, arrayBuffers);
}

acquireInputLock(): number {
return this.#input.acquireInputLock();
}
Expand Down Expand Up @@ -275,7 +295,13 @@ function startEmulator(config: EmulatorWorkerConfig) {
function () {
FS.mkdir("/Shared");
FS.mkdir("/Shared/Downloads");

FS.mkdir(PERSISTED_DIRECTORY_PATH);
if (config.persistedData) {
restorePersistedData(
PERSISTED_DIRECTORY_PATH,
config.persistedData
);
}
initializeExtractor();

for (const [name, buffer] of Object.entries(
Expand Down

0 comments on commit 0899205

Please sign in to comment.