Skip to content

Commit 0899205

Browse files
committed
Persist some data in the simulator across runs.
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.
1 parent c786938 commit 0899205

9 files changed

+145
-9
lines changed

macemu

package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@types/react-dom": "^17.0.0",
1515
"file-saver": "^2.0.5",
1616
"husky": "^6.0.0",
17+
"idb-keyval": "^6.0.3",
1718
"jszip": "^3.7.0",
1819
"lint-staged": "^11.0.0",
1920
"prettier": "^2.3.1",

src/BasiliskII/emulator-common.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const InputBufferAddresses = {
77
keyEventFlagAddr: 5,
88
keyCodeAddr: 6,
99
keyStateAddr: 7,
10+
stopFlagAddr: 8,
1011
};
1112

1213
export type EmulatorMouseEvent =
@@ -18,11 +19,15 @@ export type EmulatorKeyboardEvent = {
1819
type: "keydown" | "keyup";
1920
keyCode: number;
2021
};
22+
export type EmulatorStopEvent = {
23+
type: "stop";
24+
};
2125

2226
export type EmulatorInputEvent =
2327
| EmulatorMouseEvent
2428
| EmulatorTouchEvent
25-
| EmulatorKeyboardEvent;
29+
| EmulatorKeyboardEvent
30+
| EmulatorStopEvent;
2631

2732
export enum LockStates {
2833
READY_FOR_UI_THREAD,
@@ -52,6 +57,7 @@ export type EmulatorWorkerConfig = {
5257
wasmUrl: string;
5358
disk: EmulatorChunkedFileSpec;
5459
autoloadFiles: {[name: string]: ArrayBuffer};
60+
persistedData?: EmulatorWorkerDirectorExtraction;
5561
arguments: string[];
5662
video: EmulatorWorkerVideoConfig;
5763
input: EmulatorWorkerInputConfig;
@@ -166,6 +172,7 @@ export function updateInputBufferWithEvents(
166172
let hasKeyEvent = false;
167173
let keyCode = -1;
168174
let keyState = -1;
175+
let hasStop = false;
169176
// currently only one key event can be sent per sync
170177
// TODO: better key handling code
171178
const remainingEvents: EmulatorInputEvent[] = [];
@@ -203,6 +210,9 @@ export function updateInputBufferWithEvents(
203210
keyState = inputEvent.type === "keydown" ? 1 : 0;
204211
keyCode = inputEvent.keyCode;
205212
break;
213+
case "stop":
214+
hasStop = true;
215+
break;
206216
}
207217
}
208218
if (hasMouseMove) {
@@ -217,6 +227,9 @@ export function updateInputBufferWithEvents(
217227
inputBufferView[InputBufferAddresses.keyCodeAddr] = keyCode;
218228
inputBufferView[InputBufferAddresses.keyStateAddr] = keyState;
219229
}
230+
if (hasStop) {
231+
inputBufferView[InputBufferAddresses.stopFlagAddr] = 1;
232+
}
220233
return remainingEvents;
221234
}
222235
export type EmulatorWorkerDirectorExtractionEntry =
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type {EmulatorWorkerDirectorExtraction} from "./emulator-common";
2+
import * as idbKeyVal from "idb-keyval";
3+
4+
export async function persistData(
5+
extraction: EmulatorWorkerDirectorExtraction
6+
) {
7+
await idbKeyVal.set(EXTRACTION_KEY, extraction);
8+
}
9+
10+
export async function getPersistedData(): Promise<
11+
EmulatorWorkerDirectorExtraction | undefined
12+
> {
13+
const extraction = await idbKeyVal.get<EmulatorWorkerDirectorExtraction>(
14+
EXTRACTION_KEY
15+
);
16+
return extraction;
17+
}
18+
const EXTRACTION_KEY = "extraction";

src/BasiliskII/emulator-ui.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
EmulatorChunkedFileSpec,
33
EmulatorFallbackCommand,
44
EmulatorWorkerConfig,
5+
EmulatorWorkerDirectorExtraction,
56
EmulatorWorkerVideoBlit,
67
} from "./emulator-common";
78
import Worker from "worker-loader!./emulator-worker";
@@ -31,6 +32,7 @@ import {
3132
import {handleDirectoryExtraction} from "./emulator-ui-extractor";
3233
import BasiliskIIPath from "./BasiliskII.jsz";
3334
import BasiliskIIWasmPath from "./BasiliskII.wasmz";
35+
import {getPersistedData, persistData} from "./emulator-ui-persistence";
3436

3537
export type EmulatorConfig = {
3638
useTouchEvents: boolean;
@@ -169,6 +171,8 @@ export class Emulator {
169171
}
170172
);
171173

174+
const extraction = await getPersistedData();
175+
172176
const config: EmulatorWorkerConfig = {
173177
jsUrl: jsBlobUrl,
174178
wasmUrl: wasmBlobUrl,
@@ -177,6 +181,7 @@ export class Emulator {
177181
"Quadra-650.rom": rom,
178182
"prefs": prefs,
179183
},
184+
persistedData: extraction,
180185
arguments: ["--config", "prefs"],
181186

182187
video: this.#video.workerConfig(),
@@ -213,10 +218,7 @@ export class Emulator {
213218
this.#handleVisibilityChange
214219
);
215220

216-
this.#worker.removeEventListener("message", this.#handleWorkerMessage);
217-
218-
this.#worker.terminate();
219-
this.#workerTerminated = true;
221+
this.#input.handleInput({type: "stop"});
220222
}
221223

222224
uploadFile(file: File) {
@@ -317,6 +319,8 @@ export class Emulator {
317319
handleDirectoryExtraction(e.data.extraction);
318320
} else if (e.data.type === "emulator_first_idlewait") {
319321
console.timeEnd("Emulator first idlewait");
322+
} else if (e.data.type === "emulator_stopped") {
323+
this.#handleEmulatorStopped(e.data.extraction);
320324
} else {
321325
console.warn("Unexpected postMessage event", e);
322326
}
@@ -394,6 +398,13 @@ export class Emulator {
394398
this.#handleServiceWorkerMessage
395399
);
396400
}
401+
402+
async #handleEmulatorStopped(extraction: EmulatorWorkerDirectorExtraction) {
403+
await persistData(extraction);
404+
this.#worker.removeEventListener("message", this.#handleWorkerMessage);
405+
this.#worker.terminate();
406+
this.#workerTerminated = true;
407+
}
397408
}
398409

399410
async function load(

src/BasiliskII/emulator-worker-extractor.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ const EXTRACTOR_DIRECTORY = "/Shared/Uploads";
4040
const extractedDirectories = new Set<string>();
4141
const extractedFiles = new Set<string>();
4242

43-
function extractDirectory(dirPath: string) {
43+
export function prepareDirectoryExtraction(
44+
dirPath: string
45+
): [EmulatorWorkerDirectorExtraction, ArrayBuffer[]] {
4446
const {name: dirName, parentPath: dirParentPath} = FS.analyzePath(dirPath);
4547
const arrayBuffers: ArrayBuffer[] = [];
4648
const extraction: EmulatorWorkerDirectorExtraction = {
@@ -94,6 +96,12 @@ function extractDirectory(dirPath: string) {
9496
});
9597
}
9698

99+
return [extraction, arrayBuffers];
100+
}
101+
102+
function extractDirectory(dirPath: string) {
103+
const [extraction, arrayBuffers] = prepareDirectoryExtraction(dirPath);
104+
97105
self.postMessage(
98106
{type: "emulator_extract_directory", extraction},
99107
arrayBuffers
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type {
2+
EmulatorWorkerDirectorExtraction,
3+
EmulatorWorkerDirectorExtractionEntry,
4+
} from "./emulator-common";
5+
6+
export function restorePersistedData(
7+
dirPath: string,
8+
extraction: EmulatorWorkerDirectorExtraction
9+
) {
10+
function restore(
11+
parentDirPath: string,
12+
entries: EmulatorWorkerDirectorExtractionEntry[]
13+
) {
14+
for (const entry of entries) {
15+
if (Array.isArray(entry.contents)) {
16+
const childDirPath = `${parentDirPath}/${entry.name}`;
17+
FS.mkdir(childDirPath);
18+
restore(childDirPath, entry.contents);
19+
} else {
20+
let entryParentDirPath = parentDirPath;
21+
let entryName = entry.name;
22+
if (entryName === "DInfo") {
23+
// Write the DInfo struct for this folder in the parent's .finf
24+
// directory.
25+
entryName = extraction.name;
26+
entryParentDirPath = `${
27+
FS.analyzePath(dirPath).parentPath
28+
}/.finf`;
29+
if (!FS.analyzePath(entryParentDirPath).exists) {
30+
FS.mkdir(entryParentDirPath);
31+
}
32+
}
33+
FS.createDataFile(
34+
entryParentDirPath,
35+
entryName,
36+
entry.contents,
37+
true,
38+
true,
39+
true
40+
);
41+
}
42+
}
43+
}
44+
45+
restore(dirPath, extraction.contents);
46+
}

src/BasiliskII/emulator-worker.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@ import {createLazyFile} from "./emulator-worker-lazy-file";
3333
import {
3434
handleExtractionRequests,
3535
initializeExtractor,
36+
prepareDirectoryExtraction,
3637
} from "./emulator-worker-extractor";
3738
import {
3839
createChunkedFile,
3940
validateSpecPrefetchChunks,
4041
} from "./emulator-worker-chunked-file";
42+
import {restorePersistedData} from "./emulator-worker-persistence";
4143

4244
declare const Module: EmscriptenModule;
4345
declare const workerCommands: EmulatorFallbackCommand[];
4446

47+
const PERSISTED_DIRECTORY_PATH = "/Shared/Saved";
48+
4549
self.onmessage = function (event) {
4650
const {data} = event;
4751
const {type} = data;
@@ -64,6 +68,7 @@ class EmulatorWorkerApi {
6468
#lastIdleWaitFrameId = 0;
6569

6670
#gotFirstIdleWait = false;
71+
#handledStop = false;
6772
#diskSpec: EmulatorChunkedFileSpec;
6873

6974
constructor(config: EmulatorWorkerConfig) {
@@ -177,6 +182,10 @@ class EmulatorWorkerApi {
177182
// when the machine is idle seems reasonable.
178183
this.#handleFileUploads();
179184
handleExtractionRequests();
185+
186+
if (this.getInputValue(InputBufferAddresses.stopFlagAddr)) {
187+
this.#handleStop();
188+
}
180189
}
181190

182191
#handleFileUploads() {
@@ -193,6 +202,17 @@ class EmulatorWorkerApi {
193202
}
194203
}
195204

205+
#handleStop() {
206+
if (this.#handledStop) {
207+
return;
208+
}
209+
this.#handledStop = true;
210+
const [extraction, arrayBuffers] = prepareDirectoryExtraction(
211+
PERSISTED_DIRECTORY_PATH
212+
);
213+
postMessage({type: "emulator_stopped", extraction}, arrayBuffers);
214+
}
215+
196216
acquireInputLock(): number {
197217
return this.#input.acquireInputLock();
198218
}
@@ -275,7 +295,13 @@ function startEmulator(config: EmulatorWorkerConfig) {
275295
function () {
276296
FS.mkdir("/Shared");
277297
FS.mkdir("/Shared/Downloads");
278-
298+
FS.mkdir(PERSISTED_DIRECTORY_PATH);
299+
if (config.persistedData) {
300+
restorePersistedData(
301+
PERSISTED_DIRECTORY_PATH,
302+
config.persistedData
303+
);
304+
}
279305
initializeExtractor();
280306

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

0 commit comments

Comments
 (0)