Skip to content

Commit

Permalink
Switch to more direct disk API for the emulators
Browse files Browse the repository at this point in the history
Instead generating fiels into the virtual Emscripten file system
(that the emulators then read using POSIX APIs), expose a
workerApi.disks.* API that the emulators can use (implemented in
mihaip/minivmac@e351655 and
mihaip/macemu@4824c50).

This allows lazy loading to be handled more directly, without a
pre-allocated array of the size of the disk image. There is
now a EmulatorWorkerDisk interface with two separate implementations
(EmulatorWorkerChunkedDisk, EmulatorWorkerUploadDisk), and more
can be plugged in (e.g. for #152).

Fixes #56
  • Loading branch information
mihaip committed Apr 15, 2023
1 parent 9a2dd8c commit 9e361b9
Show file tree
Hide file tree
Showing 24 changed files with 502 additions and 368 deletions.
2 changes: 1 addition & 1 deletion minivmac
Submodule minivmac updated 1 files
+50 −65 src/OSGLUESC.c
51 changes: 28 additions & 23 deletions src/emulator/BasiliskII.jsz
Expand Up @@ -1436,29 +1436,34 @@ var tempI64;
// === Body ===

var ASM_CONSTS = {
437516: function() {return workerApi.idleWait();},
437549: function($0, $1) {workerApi.didOpenVideo($0, $1);},
437585: function() {workerApi.blit(0, 0);},
437611: function($0, $1) {workerApi.blit($0, $1);},
437639: function() {return workerApi.etherSeed();},
437673: function($0) {workerApi.etherInit(UTF8ToString($0));},
437716: function($0, $1, $2) {workerApi.etherWrite(UTF8ToString($0), $1, $2);},
437768: function($0) {return workerApi.etherRead($0, 1514);},
437810: function($0, $1) {workerApi.enqueueAudio($0, $1);},
437846: function() {return workerApi.audioBufferSize();},
437886: function($0, $1, $2, $3, $4) {workerApi.didOpenAudio($0, $1, $2, $3, $4);},
437934: function() {return workerApi.acquireInputLock();},
437975: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mouseButtonStateAddr);},
438064: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionFlagAddr);},
438154: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionXAddr);},
438241: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionYAddr);},
438328: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyEventFlagAddr);},
438413: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyCodeAddr);},
438493: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyStateAddr);},
438574: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.ethernetInterruptFlagAddr);},
438668: function() {workerApi.releaseInputLock();},
438702: function() {workerApi.sleep(0.001);},
438730: function($0) {workerApi.setClipboardText(UTF8ToString($0));}
437564: function() {return workerApi.idleWait();},
437597: function($0, $1) {workerApi.didOpenVideo($0, $1);},
437633: function() {workerApi.blit(0, 0);},
437659: function($0, $1) {workerApi.blit($0, $1);},
437687: function() {return workerApi.etherSeed();},
437721: function($0) {workerApi.etherInit(UTF8ToString($0));},
437764: function($0, $1, $2) {workerApi.etherWrite(UTF8ToString($0), $1, $2);},
437816: function($0) {return workerApi.etherRead($0, 1514);},
437858: function($0, $1) {workerApi.enqueueAudio($0, $1);},
437894: function() {return workerApi.audioBufferSize();},
437934: function($0, $1, $2, $3, $4) {workerApi.didOpenAudio($0, $1, $2, $3, $4);},
437982: function() {return workerApi.acquireInputLock();},
438023: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mouseButtonStateAddr);},
438112: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionFlagAddr);},
438202: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionXAddr);},
438289: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionYAddr);},
438376: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyEventFlagAddr);},
438461: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyCodeAddr);},
438541: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyStateAddr);},
438622: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.ethernetInterruptFlagAddr);},
438716: function() {workerApi.releaseInputLock();},
438750: function() {workerApi.sleep(0.001);},
438778: function($0) {workerApi.setClipboardText(UTF8ToString($0));},
438828: function($0) {return workerApi.disks.open(UTF8ToString($0));},
438879: function($0) {workerApi.disks.close($0);},
438910: function($0, $1, $2, $3) {return workerApi.disks.read($0, $1, $2, $3);},
438959: function($0, $1, $2, $3) {return workerApi.disks.write($0, $1, $2, $3);},
439009: function($0) {return workerApi.disks.size($0);}
};
function getClipboardText(){ const clipboardText = workerApi.getClipboardText(); if (!clipboardText || !clipboardText.length) { return 0; } const clipboardTextLength = lengthBytesUTF8(clipboardText) + 1; const clipboardTextCstr = _malloc(clipboardTextLength); stringToUTF8(clipboardText, clipboardTextCstr, clipboardTextLength); return clipboardTextCstr; }

Expand Down
Binary file modified src/emulator/BasiliskII.wasmz
Binary file not shown.
50 changes: 28 additions & 22 deletions src/emulator/SheepShaver.jsz
Expand Up @@ -1436,28 +1436,34 @@ var tempI64;
// === Body ===

var ASM_CONSTS = {
170140: function() {return workerApi.idleWait();},
170173: function($0, $1) {workerApi.didOpenVideo($0, $1);},
170209: function() {workerApi.blit(0, 0);},
170235: function($0, $1) {workerApi.blit($0, $1);},
170263: function() {return workerApi.etherSeed();},
170297: function($0) {workerApi.etherInit(UTF8ToString($0));},
170340: function($0, $1, $2) {workerApi.etherWrite(UTF8ToString($0), $1, $2);},
170392: function($0) {return workerApi.etherRead($0, 1514);},
170434: function($0, $1) {workerApi.enqueueAudio($0, $1);},
170470: function() {return workerApi.audioBufferSize();},
170510: function($0, $1, $2, $3, $4) {workerApi.didOpenAudio($0, $1, $2, $3, $4);},
170558: function() {return workerApi.acquireInputLock();},
170599: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mouseButtonStateAddr);},
170688: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionFlagAddr);},
170778: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionXAddr);},
170865: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionYAddr);},
170952: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyEventFlagAddr);},
171037: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyCodeAddr);},
171117: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyStateAddr);},
171198: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.ethernetInterruptFlagAddr);},
171292: function() {workerApi.releaseInputLock();},
171326: function($0) {workerApi.setClipboardText(UTF8ToString($0));}
170156: function() {return workerApi.idleWait();},
170189: function($0, $1) {workerApi.didOpenVideo($0, $1);},
170225: function() {workerApi.blit(0, 0);},
170251: function($0, $1) {workerApi.blit($0, $1);},
170279: function() {return workerApi.etherSeed();},
170313: function($0) {workerApi.etherInit(UTF8ToString($0));},
170356: function($0, $1, $2) {workerApi.etherWrite(UTF8ToString($0), $1, $2);},
170408: function($0) {return workerApi.etherRead($0, 1514);},
170450: function($0, $1) {workerApi.enqueueAudio($0, $1);},
170486: function() {return workerApi.audioBufferSize();},
170526: function($0, $1, $2, $3, $4) {workerApi.didOpenAudio($0, $1, $2, $3, $4);},
170574: function() {return workerApi.acquireInputLock();},
170615: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mouseButtonStateAddr);},
170704: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionFlagAddr);},
170794: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionXAddr);},
170881: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.mousePositionYAddr);},
170968: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyEventFlagAddr);},
171053: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyCodeAddr);},
171133: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.keyStateAddr);},
171214: function() {return workerApi.getInputValue(workerApi.InputBufferAddresses.ethernetInterruptFlagAddr);},
171308: function() {workerApi.releaseInputLock();},
171342: function() {workerApi.sleep(0.001);},
171370: function($0) {workerApi.setClipboardText(UTF8ToString($0));},
171420: function($0) {return workerApi.disks.open(UTF8ToString($0));},
171471: function($0) {workerApi.disks.close($0);},
171502: function($0, $1, $2, $3) {return workerApi.disks.read($0, $1, $2, $3);},
171551: function($0, $1, $2, $3) {return workerApi.disks.write($0, $1, $2, $3);},
171601: function($0) {return workerApi.disks.size($0);}
};
function getClipboardText(){ const clipboardText = workerApi.getClipboardText(); if (!clipboardText || !clipboardText.length) { return 0; } const clipboardTextLength = lengthBytesUTF8(clipboardText) + 1; const clipboardTextCstr = _malloc(clipboardTextLength); stringToUTF8(clipboardText, clipboardTextCstr, clipboardTextLength); return clipboardTextCstr; }

Expand Down
Binary file modified src/emulator/SheepShaver.wasmz
Binary file not shown.
2 changes: 1 addition & 1 deletion src/emulator/emulator-ui.ts
Expand Up @@ -328,7 +328,7 @@ export class Emulator {
prefsStr = `disk ${spec.name}\n` + prefsStr;
}
for (const diskImage of this.#diskImages) {
prefsStr += `cdrom /${diskImage.name}\n`;
prefsStr += `cdrom ${diskImage.name}\n`;
}
if (this.#config.ethernetProvider) {
prefsStr += "appletalk true\n";
Expand Down
141 changes: 141 additions & 0 deletions src/emulator/emulator-worker-chunked-disk.ts
@@ -0,0 +1,141 @@
import type {EmulatorChunkedFileSpec} from "./emulator-common";
import {generateChunkUrl} from "./emulator-common";
import type {EmulatorWorkerDisk} from "./emulator-worker-disks";

export class EmulatorWorkerChunkedDisk implements EmulatorWorkerDisk {
#spec: EmulatorChunkedFileSpec;
#loadedChunks = new Map<number, Uint8Array>();

constructor(spec: EmulatorChunkedFileSpec) {
this.#spec = spec;
}

get name(): string {
return this.#spec.name;
}

get size(): number {
return this.#spec.totalSize;
}

read(buffer: Uint8Array, offset: number, length: number): number {
let readSize = 0;
this.#forEachChunkInRange(
offset,
length,
(chunk, chunkStart, chunkEnd) => {
const chunkOffset = offset - chunkStart;
const chunkLength = Math.min(chunkEnd - offset, length);
buffer.set(
chunk.subarray(chunkOffset, chunkOffset + chunkLength),
readSize
);
offset += chunkLength;
length -= chunkLength;
readSize += chunkLength;
}
);
return readSize;
}

write(buffer: Uint8Array, offset: number, length: number): number {
let writeSize = 0;
this.#forEachChunkInRange(
offset,
length,
(chunk, chunkStart, chunkEnd) => {
const chunkOffset = offset - chunkStart;
const chunkLength = Math.min(chunkEnd - offset, length);
chunk.set(
buffer.subarray(writeSize, writeSize + chunkLength),
chunkOffset
);
offset += chunkLength;
length -= chunkLength;
writeSize += chunkLength;
}
);
return writeSize;
}

#forEachChunkInRange(
offset: number,
length: number,
callback: (
chunk: Uint8Array,
chunkStart: number,
chunkEnd: number
) => void
) {
length = Math.min(this.size - offset, length);
const {chunkSize} = this.#spec;
const startChunk = Math.floor(offset / chunkSize);
const endChunk = Math.floor((offset + length - 1) / chunkSize);
for (
let chunkIndex = startChunk;
chunkIndex <= endChunk;
chunkIndex++
) {
let chunk = this.#loadedChunks.get(chunkIndex);
if (!chunk) {
chunk = this.#loadChunk(chunkIndex);
this.#loadedChunks.set(chunkIndex, chunk);
}
if (!this.#loadedChunks.has(chunkIndex)) {
this.#loadChunk(chunkIndex);
}
callback(
chunk,
chunkIndex * chunkSize,
(chunkIndex + 1) * chunkSize
);
}
}

#loadChunk(chunkIndex: number): Uint8Array {
const chunkUrl = generateChunkUrl(this.#spec, chunkIndex);
const xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("GET", chunkUrl, false);
xhr.send();
return new Uint8Array(xhr.response as ArrayBuffer);
}

validate(): void {
const spec = this.#spec;
const prefetchedChunks = new Set(spec.prefetchChunks);
const needsPrefetch = [];
const extraPrefetch = [];
for (const chunk of this.#loadedChunks.keys()) {
if (!prefetchedChunks.has(chunk)) {
needsPrefetch.push(chunk);
}
}
for (const chunk of prefetchedChunks) {
if (!this.#loadedChunks.has(chunk)) {
extraPrefetch.push(chunk);
}
}
const numberCompare = (a: number, b: number): number => a - b;
if (extraPrefetch.length || needsPrefetch.length) {
const prefix = `Chunked file "${spec.name}" (mounted at "${spec.baseUrl}")`;
if (extraPrefetch.length) {
console.warn(
`${prefix} had unncessary chunks prefetched:`,
extraPrefetch.sort(numberCompare)
);
}
if (needsPrefetch.length) {
console.warn(
`${prefix} needs more chunks prefetched:`,
needsPrefetch.sort(numberCompare)
);
}
console.warn(
`${prefix} complete set of ideal prefetch chunks: ${JSON.stringify(
Array.from(this.#loadedChunks.keys()).sort(numberCompare)
)}`
);
}
}
}

0 comments on commit 9e361b9

Please sign in to comment.